game-maker / suite (push) Successful in 9m18s
Nine tabs need 631 px; at 1280x800 the centre column offers 666 px, so the tenth tab would have spilled under the inspector - the same overflow CI caught in run 5. TabStrip replaces the bare HStack: it fits the longest prefix that stays inside the budget, always appends the active tab, and offers the rest behind a fixed 28 px button. The button's width is fixed on purpose - deriving it from the number of hidden tabs would make width depend on the count and the count on the width. The rule itself is a pure function (tabRow.ts), so it is checked without a browser. setActive() calls relayout() itself because Widget.resize only lays out on a changed size, and switching tabs does not change any size. Adding a tab now needs no space arithmetic - only a line in TAB_ORDER, TAB_LABELS and panes, all three Record<CentreTab, ...>, so forgetting one is a type error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
439 lines
21 KiB
TypeScript
439 lines
21 KiB
TypeScript
import { type Application, Color, type ColorSource, type Container, Mesh, Rectangle, Text } from 'pixi.js';
|
||
import { resolveObjects } from '../core/model/instance';
|
||
import { findObjectHolder } from '../core/model/Project';
|
||
import type { FrameRect } from '../core/model/texture';
|
||
import { activeDrag, Button, ui, Widget } from '../ui';
|
||
import { shaderLibrary } from '../viewport/ShaderLibrary';
|
||
import { textureLibrary } from '../viewport/TextureLibrary';
|
||
import type { CentreTab, Editor } from './Editor';
|
||
import { currentFontPicker } from './font/FontPicker';
|
||
import { currentImagePicker } from './texture/ImagePicker';
|
||
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.
|
||
*/
|
||
export function installDebugHook(app: Application, state: EditorState, editor: Editor): void {
|
||
const { viewport, gameView, graphEditor, textureEditor, containerEditor, hierarchy, assets, inspector } =
|
||
editor;
|
||
const centreOf = (widget: { getGlobalPosition(): { x: number; y: number }; w: number; h: number }) => {
|
||
const p = widget.getGlobalPosition();
|
||
return { x: p.x + widget.w / 2, y: p.y + widget.h / 2 };
|
||
};
|
||
/** 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 }[] = [];
|
||
const walk = (node: Container): void => {
|
||
if (!node.visible) return;
|
||
if (node instanceof Text && node.text === 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();
|
||
hits.push({ x: box.x + box.width / 2, y: box.y + box.height / 2 });
|
||
return;
|
||
}
|
||
for (const child of node.children) walk(child);
|
||
};
|
||
walk(app.stage);
|
||
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;
|
||
};
|
||
/** Fill colour of a visible text, as `#rrggbb` – what tells an override label from a plain one. */
|
||
const textColor = (text: string): string | null => {
|
||
let found: string | null = null;
|
||
const walk = (node: Container): void => {
|
||
if (found !== null || !node.visible) return;
|
||
if (node instanceof Text && node.text === text) {
|
||
found = new Color(node.style.fill as ColorSource).toHex();
|
||
return;
|
||
}
|
||
for (const child of node.children) walk(child);
|
||
};
|
||
walk(app.stage);
|
||
return found;
|
||
};
|
||
/**
|
||
* Centre of the control of an inspector row, addressed by its label. A row aligns its label to a
|
||
* *minimum* width, so a label wider than the column pushes its control right – which is why a
|
||
* fixed offset from the label's own text is the wrong way to click one.
|
||
*/
|
||
const fieldOf = (label: string, last = false): { x: number; y: number; left: number } | null => {
|
||
let found: Text | null = null;
|
||
const walk = (node: Container): void => {
|
||
if ((found !== null && !last) || !node.visible) return;
|
||
if (node instanceof Text && node.text === label) {
|
||
found = node;
|
||
return;
|
||
}
|
||
for (const child of node.children) walk(child);
|
||
};
|
||
walk(app.stage);
|
||
// TypeScript does not track the assignment inside `walk`, hence the cast.
|
||
let owner: Container | null = (found as Text | null)?.parent ?? null;
|
||
while (owner && !(owner instanceof Widget)) owner = owner.parent;
|
||
const row = owner?.parent;
|
||
if (!row || !owner) return null;
|
||
const control = row.children[row.children.indexOf(owner) + 1];
|
||
if (!(control instanceof Widget)) return null;
|
||
return { ...centreOf(control), left: control.getGlobalPosition().x };
|
||
};
|
||
/**
|
||
* The visible text of the overlay layer, in drawing order – everything an open popup shows.
|
||
* Menus hide filtered-out entries, so this is also what a search box currently leaves.
|
||
*/
|
||
const overlayTexts = (): string[] => {
|
||
const out: string[] = [];
|
||
const walk = (node: Container): void => {
|
||
if (!node.visible) return;
|
||
if (node instanceof Text) {
|
||
if (node.text) out.push(node.text);
|
||
return;
|
||
}
|
||
for (const child of node.children) walk(child);
|
||
};
|
||
walk(ui().overlay);
|
||
return out;
|
||
};
|
||
/**
|
||
* 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 }[] = [];
|
||
const walk = (node: Container, owner: Widget | null, clipped: boolean): void => {
|
||
if (!node.visible) return;
|
||
const isWidget = node instanceof Widget;
|
||
const box = isWidget ? node : owner;
|
||
// 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;
|
||
const text = node.getBounds();
|
||
const origin = box.getGlobalPosition();
|
||
const over = Math.max(text.x + text.width - (origin.x + box.w), origin.x - text.x);
|
||
if (over > 1 || all) {
|
||
out.push({ text: node.text, owner: box.constructor.name, over: Math.round(over) });
|
||
}
|
||
return;
|
||
}
|
||
for (const child of node.children) walk(child, box, masked);
|
||
};
|
||
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;
|
||
};
|
||
/** What the last visible button labelled `text` draws – shorter when it was truncated. */
|
||
const shownLabel = (text: string): string | null => {
|
||
let shown: string | null = null;
|
||
const walk = (node: Container): void => {
|
||
if (!node.visible) return;
|
||
if (node instanceof Button && node.text === text) shown = node.shownText;
|
||
for (const child of node.children) walk(child);
|
||
};
|
||
walk(app.stage);
|
||
return shown;
|
||
};
|
||
/** 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);
|
||
const popup = popups[popups.length - 1];
|
||
if (!popup) return null;
|
||
const p = popup.getGlobalPosition();
|
||
return { x: p.x, y: p.y, w: popup.w, h: popup.h };
|
||
};
|
||
/**
|
||
* 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) : [];
|
||
};
|
||
(window as unknown as { __gm: unknown }).__gm = {
|
||
state,
|
||
/** The Pixi application, for probes that need the renderer or the stage. */
|
||
app,
|
||
sceneObjects: () => (state.activeScene ? objectsOf(state.activeScene.id) : []),
|
||
locate,
|
||
locateAll,
|
||
inspectorField: fieldOf,
|
||
inspectorInfo: () => inspector.debugInfo(),
|
||
textColor,
|
||
overlayTexts,
|
||
overlayRect,
|
||
textOverflows,
|
||
labelsDropped,
|
||
shownLabel,
|
||
tabRow: () => editor.tabRow(),
|
||
/** The dividers left and right of the centre column – dragging them narrows the tab row. */
|
||
panelSplitters: () => ({ left: centreOf(editor.leftSplitter), right: centreOf(editor.rightSplitter) }),
|
||
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(),
|
||
/** Where a dragged asset would land in the Scene tab, and why not when it may not. */
|
||
dropPreview: () => viewport.dropInfo(),
|
||
/** The panel-to-view drag in flight, if any – a drop that never started looks like a refused one. */
|
||
dragPayload: () => activeDrag(),
|
||
/** An asset row by id – two assets can share a name with the container picker's label. */
|
||
assetRow: (id: string) => assets.rowGlobal(id),
|
||
assetRowColor: (id: string) => assets.rowColor(id),
|
||
gameInfo: () => gameView.debugInfo(),
|
||
/** Selection outlines in the Scene tab: a switched-off object is invisible but still framed. */
|
||
gizmos: () => viewport.gizmoInfo(),
|
||
layerOf: (id: string) => viewport.layerOf(id),
|
||
drawOrder: () => viewport.drawOrder(),
|
||
objectScreen: (id: string) => viewport.objectScreen(id),
|
||
objectRect: (id: string) => viewport.objectRect(id),
|
||
/** 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),
|
||
graph: {
|
||
current: () => graphEditor.currentAssetId,
|
||
portAt: (nodeId: string, port: string, isOutput: boolean) =>
|
||
graphEditor.canvas.portGlobal(nodeId, port, isOutput),
|
||
nodeAt: (nodeId: string) => graphEditor.canvas.nodeGlobal(nodeId),
|
||
chevronAt: (nodeId: string) => graphEditor.canvas.chevronGlobal(nodeId),
|
||
nodeLabel: (nodeId: string) => graphEditor.canvas.nodeLabel(nodeId),
|
||
nodeProblem: (nodeId: string) => graphEditor.canvas.nodeProblem(nodeId),
|
||
nodeSettings: (nodeId: string) => graphEditor.canvas.nodeSettings(nodeId),
|
||
/** Each input row as drawn: its name, and the inline value beside it while unconnected. */
|
||
nodeInputs: (nodeId: string) => graphEditor.canvas.nodeInputs(nodeId),
|
||
/** Each output row as drawn: while playing, the value it last carried in front of its name. */
|
||
nodeOutputs: (nodeId: string) => graphEditor.canvas.nodeOutputs(nodeId),
|
||
/** Beads in flight on exec edges as drawn, in graph coordinates; `progress` 0 = source, 1 = target. */
|
||
beads: () => graphEditor.canvas.beadsDrawn(),
|
||
/** Drawn rectangles on screen: what "fit" has to keep inside `canvasRect`. */
|
||
nodeRect: (nodeId: string) => graphEditor.canvas.nodeRect(nodeId),
|
||
canvasRect: () => graphEditor.canvas.canvasRect(),
|
||
/** Bare canvas below every node – where a step right-clicks to open the palette. */
|
||
emptySpot: () => graphEditor.canvas.emptySpotGlobal(),
|
||
selected: () => state.selectedNodeIds,
|
||
dropTarget: () => graphEditor.canvas.pendingDropTarget(),
|
||
danglingEdge: () => graphEditor.canvas.danglingEdgeVisible(),
|
||
edgeDrawn: (edgeId: string) => graphEditor.canvas.edgeDrawn(edgeId),
|
||
foldMark: (edgeId: string, isOutput: boolean) => graphEditor.canvas.foldMarkGlobal(edgeId, isOutput),
|
||
foldMarks: () => graphEditor.canvas.foldMarksDrawn(),
|
||
// The canvas, not the whole tab: a centre measured through the header sits above the
|
||
// drawing area.
|
||
center: () => centreOf(graphEditor.canvas),
|
||
pickerWidth: () => graphEditor.picker.w,
|
||
pickerAt: () => centreOf(graphEditor.picker),
|
||
snapping: () => graphEditor.canvas.snapEnabled,
|
||
zoom: () => graphEditor.canvas.zoomLevel,
|
||
crumbs: () => graphEditor.breadcrumb.crumbsDrawn(),
|
||
},
|
||
stateMachine: {
|
||
current: () => editor.stateMachineEditor.currentAssetId,
|
||
pickerAt: () => centreOf(editor.stateMachineEditor.picker),
|
||
nodeAt: (nodeId: string) => editor.stateMachineEditor.canvas.nodeGlobal(nodeId),
|
||
nodeLabel: (nodeId: string) => editor.stateMachineEditor.canvas.nodeLabel(nodeId),
|
||
nodeRect: (nodeId: string) => editor.stateMachineEditor.canvas.nodeRect(nodeId),
|
||
canvasRect: () => editor.stateMachineEditor.canvas.canvasRect(),
|
||
center: () => centreOf(editor.stateMachineEditor.canvas),
|
||
snapping: () => editor.stateMachineEditor.canvas.snapEnabled,
|
||
},
|
||
shader: {
|
||
current: () => editor.shaderEditor.currentAssetId,
|
||
pickerAt: () => centreOf(editor.shaderEditor.picker),
|
||
info: () => editor.shaderEditor.shownInfo,
|
||
nodeAt: (nodeId: string) => editor.shaderEditor.canvas.nodeGlobal(nodeId),
|
||
nodeLabel: (nodeId: string) => editor.shaderEditor.canvas.nodeLabel(nodeId),
|
||
nodeRect: (nodeId: string) => editor.shaderEditor.canvas.nodeRect(nodeId),
|
||
nodeProblem: (nodeId: string) => editor.shaderEditor.canvas.nodeProblem(nodeId),
|
||
nodeSettings: (nodeId: string) => editor.shaderEditor.canvas.nodeSettings(nodeId),
|
||
canvasRect: () => editor.shaderEditor.canvas.canvasRect(),
|
||
emptySpot: () => editor.shaderEditor.canvas.emptySpotGlobal(),
|
||
center: () => centreOf(editor.shaderEditor.canvas),
|
||
snapping: () => editor.shaderEditor.canvas.snapEnabled,
|
||
},
|
||
/** The console panel: its lines as kept, its drawn height, and the divider above it. */
|
||
console: {
|
||
lines: () => editor.console.debugInfo().lines,
|
||
height: () => editor.console.h,
|
||
clear: () => editor.console.clear(),
|
||
scroll: () => editor.console.scrollInfo(),
|
||
centre: () => centreOf(editor.console),
|
||
splitterAt: () => centreOf(editor.consoleSplitter),
|
||
},
|
||
/** The open image picker: what it offers, what is picked, and where a tile sits. */
|
||
imagePicker: () => currentImagePicker()?.debugInfo() ?? null,
|
||
imagePickerTile: (file: string) => currentImagePicker()?.tileGlobal(file) ?? null,
|
||
/** The same for the font picker, whose rows are the preview. */
|
||
fontPicker: () => currentFontPicker()?.debugInfo() ?? null,
|
||
fontPickerRow: (file: string) => currentFontPicker()?.rowGlobal(file) ?? null,
|
||
/** 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),
|
||
texture: {
|
||
current: () => textureEditor.currentAssetId,
|
||
pickerWidth: () => textureEditor.picker.w,
|
||
/** 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),
|
||
rect: () => textureEditor.canvasRect(),
|
||
},
|
||
animation: {
|
||
/** Frames, the one on screen, play state and where the preview picture is drawn. */
|
||
current: () => editor.animationEditor.canvas.debugInfo(),
|
||
tileCentre: (index: number) => editor.animationEditor.canvas.tileCentre(index),
|
||
playButton: () => centreOf(editor.animationEditor.playButton),
|
||
playLabel: () => editor.animationEditor.playButton.text,
|
||
},
|
||
container: {
|
||
current: () => containerEditor.currentAssetId,
|
||
pickerWidth: () => containerEditor.picker.w,
|
||
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(),
|
||
},
|
||
list: {
|
||
info: () => editor.listEditor.debugInfo(),
|
||
/** Where an item's widget sits, so a check can click it – a number field has no findable text. */
|
||
fieldAt: (index: number, field = '') => editor.listEditor.fieldAt(index, field),
|
||
/** The kind mark of a row, or of one field of a struct item – both are pickers. */
|
||
markAt: (index: number, field = '') => editor.listEditor.markAt(index, field),
|
||
nameAt: (index: number, field: string) => editor.listEditor.nameAt(index, field),
|
||
},
|
||
world: () => editor.worldSnapshot(),
|
||
playInfo: () => editor.playInfo(),
|
||
/** Behaviors running without Play (`Run in editor`), and how long they have been at it. */
|
||
editorRun: () => editor.editorRunInfo(),
|
||
/** Rows the hierarchy shows – collapsed subtrees are not in here. */
|
||
hierarchyOrder: () => hierarchy.rowOrder(),
|
||
hierarchyChevron: (id: string) => hierarchy.chevronPosition(id),
|
||
/** Centre of a hierarchy row, clear of the chevron – for clicks that must hit the row itself. */
|
||
hierarchyRow: (id: string) => hierarchy.rowPosition(id),
|
||
hierarchyRowColor: (id: string) => hierarchy.rowColor(id),
|
||
hierarchyExpandAll: () => hierarchy.expandAll(),
|
||
hierarchyCollapseAll: () => hierarchy.collapseAll(),
|
||
/** 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,
|
||
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])}`;
|
||
},
|
||
/** 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,
|
||
};
|
||
},
|
||
fps: () => editor.fps,
|
||
/** 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;
|
||
},
|
||
/** 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),
|
||
/** 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,
|
||
tab: () => editor.tab,
|
||
showTab: (tab: CentreTab) => editor.showTab(tab),
|
||
};
|
||
}
|