@ringozz/godot 4.7.2-614 → 4.7.2-616
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -146
- package/package.json +7 -4
- package/src/assets.d.ts +130 -130
- package/src/debug.ts +357 -357
- package/src/editor.ts +107 -0
- package/src/index.ts +54 -50
- package/src/load.ts +231 -231
- package/src/preload.ts +253 -253
- package/src/runtime.ts +107 -107
- package/src/web-image.ts +153 -117
package/src/debug.ts
CHANGED
|
@@ -1,357 +1,357 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
import { MouseButton, str, varToStr } from './index.ts';
|
|
6
|
-
import { CanvasItem } from '../gen/classes/CanvasItem.ts';
|
|
7
|
-
import { ClassDB } from '../gen/classes/ClassDB.ts';
|
|
8
|
-
import { Control } from '../gen/classes/Control.ts';
|
|
9
|
-
import { DisplayServer } from '../gen/classes/DisplayServer.ts';
|
|
10
|
-
import { Engine } from '../gen/classes/Engine.ts';
|
|
11
|
-
import { InputEventMouseButton } from '../gen/classes/InputEventMouseButton.ts';
|
|
12
|
-
import { Node } from '../gen/classes/Node.ts';
|
|
13
|
-
import { Node3D } from '../gen/classes/Node3D.ts';
|
|
14
|
-
import { OS } from '../gen/classes/OS.ts';
|
|
15
|
-
import { SceneTree } from '../gen/classes/SceneTree.ts';
|
|
16
|
-
import { Time } from '../gen/classes/Time.ts';
|
|
17
|
-
import type { Vector2 } from '../gen/value-types/Vector2.ts';
|
|
18
|
-
import type { Viewport } from '../gen/classes/Viewport.ts';
|
|
19
|
-
import type { Window } from '../gen/classes/Window.ts';
|
|
20
|
-
|
|
21
|
-
const tree = Engine.getMainLoop() as SceneTree;
|
|
22
|
-
|
|
23
|
-
/** Does a snapshot line match a string (case-insensitive substring) or `RegExp` pattern? */
|
|
24
|
-
function matches(pattern: string | RegExp | undefined, line: string): boolean {
|
|
25
|
-
return !pattern || (pattern instanceof RegExp ? pattern.test(line) : line.toLowerCase().includes(pattern.toLowerCase()));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Dump a Godot value or object as a string.
|
|
30
|
-
* Value types (vectors, arrays, dictionaries, …) render via Godot's `str()`.
|
|
31
|
-
* Objects dump via Godot's `var_to_str` — its own property walk + recursion —
|
|
32
|
-
* with the node's `name` prepended when present.
|
|
33
|
-
*
|
|
34
|
-
* @param obj - Any Godot wrapper (Node, Resource, …), value type, or plain value
|
|
35
|
-
*/
|
|
36
|
-
export function dumpStr(obj: unknown): string {
|
|
37
|
-
if (obj == null || typeof obj !== 'object') return String(obj);
|
|
38
|
-
|
|
39
|
-
// Value types & heap containers (Vector3, Array, Dictionary, …) have no getClass() —
|
|
40
|
-
// let Godot's str() render them.
|
|
41
|
-
if (!(obj as any).getClass) return str(obj as any);
|
|
42
|
-
|
|
43
|
-
const dump = varToStr(obj as any);
|
|
44
|
-
let name: unknown;
|
|
45
|
-
try { name = (obj as any).name; } catch { name = undefined; }
|
|
46
|
-
return name === undefined || name === null || name === '' ? dump : `${name}: ${dump}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Serialize a scene subtree as a tree-formatted string, optionally filtered.
|
|
51
|
-
*
|
|
52
|
-
* Emits one line per node: `Class "name" (N children)`. When `pattern` is a
|
|
53
|
-
* string, only lines containing it (case-insensitive) are kept; a `RegExp`
|
|
54
|
-
* matches against the full line.
|
|
55
|
-
*
|
|
56
|
-
* @param pattern - Optional substring or `RegExp` to filter the lines
|
|
57
|
-
* @param node - Root node to start from (defaults to the scene tree's root Window)
|
|
58
|
-
* @param depth - Max depth (default 99)
|
|
59
|
-
*/
|
|
60
|
-
export function snapshot(pattern?: string | RegExp, node?: Node, depth = 99): string {
|
|
61
|
-
const root = node ?? tree.root;
|
|
62
|
-
const lines: string[] = [];
|
|
63
|
-
(function walk(n: Node, d: number, ind: string) {
|
|
64
|
-
if (d <= 0) return;
|
|
65
|
-
const count = n.getChildCount();
|
|
66
|
-
const line = `${ind}${n.getClass()} "${n.name}" (${count} children)`;
|
|
67
|
-
if (matches(pattern, line)) lines.push(line);
|
|
68
|
-
for (let i = 0; i < count; i++) {
|
|
69
|
-
const c = n.getChild(i);
|
|
70
|
-
if (c) walk(c as Node, d - 1, ind + ' ');
|
|
71
|
-
}
|
|
72
|
-
})(root, depth, '');
|
|
73
|
-
return lines.join('\n');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Resolve a node from the scene root by absolute NodePath (`/root/...`) or
|
|
78
|
-
* name glob (`findChild`). Returns `null` when not found.
|
|
79
|
-
*/
|
|
80
|
-
function resolveNode(path: string, root: Window): Node | null {
|
|
81
|
-
if (path.startsWith('/')) return root.getNode(path);
|
|
82
|
-
return root.findChild(path, true, false);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Resolve a node by path (absolute NodePath or name glob) from the scene root
|
|
87
|
-
* and dump it via {@link dumpStr}.
|
|
88
|
-
*
|
|
89
|
-
* @param path - An absolute NodePath like `/root/Box`, or a `findChild` name/glob pattern
|
|
90
|
-
*/
|
|
91
|
-
export function inspect(path: string): string | null {
|
|
92
|
-
const node = resolveNode(path, tree.root);
|
|
93
|
-
return node ? dumpStr(node) : null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Synthesize a left-click at the center of a {@link Control} matched by `path`.
|
|
98
|
-
*
|
|
99
|
-
* Press and release are pushed through `Window.pushInput` (the GUI-routing
|
|
100
|
-
* path — `Input.parseInputEvent` doesn't reliably reach `Control.guiInput` on
|
|
101
|
-
* the wasm build). Positions are in device space (logical × `contentScaleFactor`).
|
|
102
|
-
*
|
|
103
|
-
* @param path - An absolute NodePath like `/root/...`, or a `findChild` name/glob pattern
|
|
104
|
-
* @returns The node's `dumpStr`, or `null` if not found or not a `Control`
|
|
105
|
-
*/
|
|
106
|
-
export function click(path: string): string | null {
|
|
107
|
-
const root = tree.root;
|
|
108
|
-
const node = resolveNode(path, root);
|
|
109
|
-
if (!(node instanceof Control)) return null;
|
|
110
|
-
const rect = node.getGlobalRect();
|
|
111
|
-
const scale = root.contentScaleFactor;
|
|
112
|
-
const position: Vector2 = [
|
|
113
|
-
(rect[0] + rect[2] / 2) * scale,
|
|
114
|
-
(rect[1] + rect[3] / 2) * scale,
|
|
115
|
-
];
|
|
116
|
-
const press = (down: boolean) => {
|
|
117
|
-
const event = new InputEventMouseButton();
|
|
118
|
-
event.position = position;
|
|
119
|
-
event.globalPosition = position;
|
|
120
|
-
event.buttonIndex = MouseButton.MOUSE_BUTTON_LEFT;
|
|
121
|
-
event.pressed = down;
|
|
122
|
-
root.pushInput(event);
|
|
123
|
-
};
|
|
124
|
-
press(true);
|
|
125
|
-
setTimeout(() => press(false), 50);
|
|
126
|
-
return dumpStr(node);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Enumerate nodes in the scene by name pattern and/or class, in tree order.
|
|
131
|
-
*
|
|
132
|
-
* @param pattern - Name glob matched by `findChildren` (default `"*"`)
|
|
133
|
-
* @param type - Class name to filter by, e.g. `"RigidBody3D"` (default `""` = any)
|
|
134
|
-
* @param node - Root node to search under (defaults to the scene tree's root Window)
|
|
135
|
-
*/
|
|
136
|
-
export function find(pattern = '*', type = '', node?: Node): Node[] {
|
|
137
|
-
const root = node ?? tree.root;
|
|
138
|
-
return [...root.findChildren(pattern, type, true, false)];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Run `gc()` and report how much heap was freed.
|
|
143
|
-
* Requires the `--expose-gc` Node.js flag.
|
|
144
|
-
*/
|
|
145
|
-
export function gc(): string {
|
|
146
|
-
const before = globalThis.process?.memoryUsage?.().heapUsed;
|
|
147
|
-
if (globalThis.Bun)
|
|
148
|
-
globalThis.Bun.gc(true);
|
|
149
|
-
else if (globalThis.gc)
|
|
150
|
-
globalThis.gc({ type: 'major', execution: 'sync' });
|
|
151
|
-
else
|
|
152
|
-
return 'gc() unavailable — need --expose-gc';
|
|
153
|
-
|
|
154
|
-
const after = globalThis.process?.memoryUsage?.().heapUsed;
|
|
155
|
-
const freed = typeof before === 'number' && typeof after === 'number' ? (before - after) / 1024 : 0;
|
|
156
|
-
return `GC: freed ${freed.toFixed(0)} KB`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* `DOMRect` is not a global in Bun (desktop), but `getBoundingClientRect()`
|
|
161
|
-
* constructs one. Install a minimal spec-compatible polyfill on desktop so the
|
|
162
|
-
* helper works identically on desktop and web (browsers already define it).
|
|
163
|
-
*/
|
|
164
|
-
function ensureDOMRect(): void {
|
|
165
|
-
if (typeof (globalThis as any).DOMRect !== 'undefined') return;
|
|
166
|
-
(globalThis as any).DOMRect = class DOMRect {
|
|
167
|
-
x: number;
|
|
168
|
-
y: number;
|
|
169
|
-
width: number;
|
|
170
|
-
height: number;
|
|
171
|
-
constructor(x = 0, y = 0, width = 0, height = 0) {
|
|
172
|
-
this.x = x;
|
|
173
|
-
this.y = y;
|
|
174
|
-
this.width = width;
|
|
175
|
-
this.height = height;
|
|
176
|
-
}
|
|
177
|
-
get left() { return this.x; }
|
|
178
|
-
get top() { return this.y; }
|
|
179
|
-
get right() { return this.x + this.width; }
|
|
180
|
-
get bottom() { return this.y + this.height; }
|
|
181
|
-
toJSON() { return { x: this.x, y: this.y, width: this.width, height: this.height }; }
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
type Push = (x: number, y: number) => void;
|
|
186
|
-
type BoundsCollect = (node: any, push: Push, viewport: Viewport) => void;
|
|
187
|
-
|
|
188
|
-
/** Affine 2D point mapper from a transform's basis/origin, optionally scaled. */
|
|
189
|
-
function makeAffine(t: any, scale = 1): (x: number, y: number) => [number, number] {
|
|
190
|
-
const [tx, ty, to] = t;
|
|
191
|
-
const tox = to[0], toy = to[1];
|
|
192
|
-
const txx = tx[0], txy = tx[1];
|
|
193
|
-
const tyx = ty[0], tyy = ty[1];
|
|
194
|
-
return (x, y) => [(tox + txx * x + tyx * y) * scale, (toy + txy * x + tyy * y) * scale];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Axis-aligned 2D bounds accumulator in CSS pixels. Non-finite points (e.g.
|
|
199
|
-
* geometry behind the camera) are dropped; `rect()` is `null` when no point
|
|
200
|
-
* was added.
|
|
201
|
-
*/
|
|
202
|
-
function cssBounds(): { add(x: number, y: number): void; rect(): [number, number, number, number] | null } {
|
|
203
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
204
|
-
let found = false;
|
|
205
|
-
return {
|
|
206
|
-
add(x, y) {
|
|
207
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
208
|
-
if (x < minX) minX = x; if (y < minY) minY = y;
|
|
209
|
-
if (x > maxX) maxX = x; if (y > maxY) maxY = y;
|
|
210
|
-
found = true;
|
|
211
|
-
},
|
|
212
|
-
rect() {
|
|
213
|
-
return found ? [minX, minY, maxX - minX, maxY - minY] : null;
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** World-space AABB (min, max) of a local AABB under its global transform, or `null` when degenerate. */
|
|
219
|
-
function worldAabb(aabb: any, gt: any): [number, number, number, number, number, number] | null {
|
|
220
|
-
const [position, size] = aabb;
|
|
221
|
-
const [bx, by, bz, origin] = gt;
|
|
222
|
-
if (size[0] === 0 && size[1] === 0 && size[2] === 0) return null;
|
|
223
|
-
const cx = position[0] + size[0] / 2;
|
|
224
|
-
const cy = position[1] + size[1] / 2;
|
|
225
|
-
const cz = position[2] + size[2] / 2;
|
|
226
|
-
const ex = Math.hypot(bx[0], bx[1], bx[2]) * size[0] / 2;
|
|
227
|
-
const ey = Math.hypot(by[0], by[1], by[2]) * size[1] / 2;
|
|
228
|
-
const ez = Math.hypot(bz[0], bz[1], bz[2]) * size[2] / 2;
|
|
229
|
-
const ox = origin[0], oy = origin[1], oz = origin[2];
|
|
230
|
-
const wx = ox + bx[0] * cx + by[0] * cy + bz[0] * cz;
|
|
231
|
-
const wy = oy + bx[1] * cx + by[1] * cy + bz[1] * cz;
|
|
232
|
-
const wz = oz + bx[2] * cx + by[2] * cy + bz[2] * cz;
|
|
233
|
-
return [wx - ex, wy - ey, wz - ez, wx + ex, wy + ey, wz + ez];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* World-space AABB of a node's visual content: the union of its own AABB (when
|
|
238
|
-
* it is a `VisualInstance3D`) and every descendant `VisualInstance3D`'s, each in
|
|
239
|
-
* its own global transform — mirrors the editor's `_calculate_spatial_bounds`.
|
|
240
|
-
* Returns `null` when there is no non-degenerate visual.
|
|
241
|
-
*/
|
|
242
|
-
function worldBounds(node: Node3D): [number, number, number, number, number, number] | null {
|
|
243
|
-
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
244
|
-
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
245
|
-
const add = (aabb: any, gt: any) => {
|
|
246
|
-
const box = worldAabb(aabb, gt);
|
|
247
|
-
if (!box) return;
|
|
248
|
-
if (box[0] < minX) minX = box[0]; if (box[3] > maxX) maxX = box[3];
|
|
249
|
-
if (box[1] < minY) minY = box[1]; if (box[4] > maxY) maxY = box[4];
|
|
250
|
-
if (box[2] < minZ) minZ = box[2]; if (box[5] > maxZ) maxZ = box[5];
|
|
251
|
-
};
|
|
252
|
-
if (typeof (node as any).getAabb === 'function') {
|
|
253
|
-
add((node as any).getAabb(), node.globalTransform);
|
|
254
|
-
}
|
|
255
|
-
for (const child of node.findChildren('*', 'VisualInstance3D', true, false) as unknown as any[]) {
|
|
256
|
-
if (child && typeof child.getAabb === 'function') {
|
|
257
|
-
add(child.getAabb(), child.globalTransform);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return minX === Infinity ? null : [minX, minY, minZ, maxX, maxY, maxZ];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Compute a node's bounding rect in CSS pixels. The callback pushes points in
|
|
265
|
-
* viewport logical units; they're mapped through the viewport's screen transform
|
|
266
|
-
* (content scale, stretch, SubViewport/child-window placement), then divided by
|
|
267
|
-
* screenGetScale to reach CSS pixels. Never throws: React DevTools calls this on
|
|
268
|
-
* every commit and must not see a throw (e.g. a wrapper freed by an unmount).
|
|
269
|
-
*/
|
|
270
|
-
function boundsForViewport(node: Node, collect: (push: Push, viewport: Viewport) => void): DOMRect | null {
|
|
271
|
-
try {
|
|
272
|
-
const viewport = node.getViewport();
|
|
273
|
-
if (!viewport) return null;
|
|
274
|
-
const xform = makeAffine(viewport.getScreenTransform(), 1 / DisplayServer.screenGetScale());
|
|
275
|
-
const bounds = cssBounds();
|
|
276
|
-
collect((x, y) => bounds.add(...xform(x, y)), viewport);
|
|
277
|
-
const r = bounds.rect();
|
|
278
|
-
return r ? new DOMRect(r[0], r[1], r[2], r[3]) : null;
|
|
279
|
-
} catch {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** Install `getBoundingClientRect` on a node prototype. */
|
|
285
|
-
function defineBoundingClientRect(proto: object, collect: BoundsCollect): void {
|
|
286
|
-
Object.defineProperty(proto, 'getBoundingClientRect', {
|
|
287
|
-
configurable: true,
|
|
288
|
-
value(this: Node): DOMRect | null {
|
|
289
|
-
return boundsForViewport(this, (push, viewport) => collect(this, push, viewport));
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** Push the four corners of an axis-aligned rect through `emit`. */
|
|
295
|
-
function pushRect(emit: (x: number, y: number) => void, x0: number, y0: number, x1: number, y1: number): void {
|
|
296
|
-
for (const px of [x0, x1]) {
|
|
297
|
-
for (const py of [y0, y1]) {
|
|
298
|
-
emit(px, py);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const canvasItemBounds: BoundsCollect = (node, push) => {
|
|
304
|
-
if (node instanceof Control) {
|
|
305
|
-
const [x, y, w, h] = node.getGlobalRect();
|
|
306
|
-
pushRect(push, x, y, x + w, y + h);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (typeof node.getRect !== 'function') return;
|
|
310
|
-
const [x, y, w, h] = node.getRect() as [number, number, number, number];
|
|
311
|
-
const xform = makeAffine(node.getScreenTransform());
|
|
312
|
-
pushRect((px, py) => push(...xform(px, py)), x, y, x + w, y + h);
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const node3dBounds: BoundsCollect = (node, push, viewport) => {
|
|
316
|
-
const camera = viewport.getCamera3d();
|
|
317
|
-
if (!camera) return;
|
|
318
|
-
const box = worldBounds(node);
|
|
319
|
-
if (!box) return;
|
|
320
|
-
const [minX, minY, minZ, maxX, maxY, maxZ] = box;
|
|
321
|
-
for (let i = 0; i < 8; i++) {
|
|
322
|
-
const s = camera.unprojectPosition([
|
|
323
|
-
i & 1 ? maxX : minX,
|
|
324
|
-
i & 2 ? maxY : minY,
|
|
325
|
-
i & 4 ? maxZ : minZ,
|
|
326
|
-
]);
|
|
327
|
-
push(s[0], s[1]);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Bootstrap the debugging environment.
|
|
333
|
-
*
|
|
334
|
-
* - Exposes common singletons (`Engine`, `OS`, `ClassDB`, `Time`) and all
|
|
335
|
-
* debug utility functions on `globalThis.$` so they're reachable from
|
|
336
|
-
* breakpoints in any module.
|
|
337
|
-
* - Registers an `uncaughtException` handler that logs but keeps the process
|
|
338
|
-
* alive for interactive debugging.
|
|
339
|
-
* - Polyfills `DOMRect` (needed by `getBoundingClientRect()` on desktop).
|
|
340
|
-
*/
|
|
341
|
-
export function initDebug(): void {
|
|
342
|
-
globalThis.process?.on?.('uncaughtException', (err, origin) => {
|
|
343
|
-
console.error(`\n✗ ${origin}:`, err.stack);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
ensureDOMRect();
|
|
347
|
-
defineBoundingClientRect(CanvasItem.prototype, canvasItemBounds);
|
|
348
|
-
defineBoundingClientRect(Node3D.prototype, node3dBounds);
|
|
349
|
-
|
|
350
|
-
(globalThis as any).$ = {
|
|
351
|
-
Engine, OS, ClassDB, Time,
|
|
352
|
-
dumpStr, snapshot, inspect, click, find, gc,
|
|
353
|
-
get tree(): SceneTree { return tree; },
|
|
354
|
-
get paused(): boolean { return tree.paused; },
|
|
355
|
-
set paused(v: boolean) { tree.paused = v; },
|
|
356
|
-
};
|
|
357
|
-
}
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
import { MouseButton, str, varToStr } from './index.ts';
|
|
6
|
+
import { CanvasItem } from '../gen/classes/CanvasItem.ts';
|
|
7
|
+
import { ClassDB } from '../gen/classes/ClassDB.ts';
|
|
8
|
+
import { Control } from '../gen/classes/Control.ts';
|
|
9
|
+
import { DisplayServer } from '../gen/classes/DisplayServer.ts';
|
|
10
|
+
import { Engine } from '../gen/classes/Engine.ts';
|
|
11
|
+
import { InputEventMouseButton } from '../gen/classes/InputEventMouseButton.ts';
|
|
12
|
+
import { Node } from '../gen/classes/Node.ts';
|
|
13
|
+
import { Node3D } from '../gen/classes/Node3D.ts';
|
|
14
|
+
import { OS } from '../gen/classes/OS.ts';
|
|
15
|
+
import { SceneTree } from '../gen/classes/SceneTree.ts';
|
|
16
|
+
import { Time } from '../gen/classes/Time.ts';
|
|
17
|
+
import type { Vector2 } from '../gen/value-types/Vector2.ts';
|
|
18
|
+
import type { Viewport } from '../gen/classes/Viewport.ts';
|
|
19
|
+
import type { Window } from '../gen/classes/Window.ts';
|
|
20
|
+
|
|
21
|
+
const tree = Engine.getMainLoop() as SceneTree;
|
|
22
|
+
|
|
23
|
+
/** Does a snapshot line match a string (case-insensitive substring) or `RegExp` pattern? */
|
|
24
|
+
function matches(pattern: string | RegExp | undefined, line: string): boolean {
|
|
25
|
+
return !pattern || (pattern instanceof RegExp ? pattern.test(line) : line.toLowerCase().includes(pattern.toLowerCase()));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Dump a Godot value or object as a string.
|
|
30
|
+
* Value types (vectors, arrays, dictionaries, …) render via Godot's `str()`.
|
|
31
|
+
* Objects dump via Godot's `var_to_str` — its own property walk + recursion —
|
|
32
|
+
* with the node's `name` prepended when present.
|
|
33
|
+
*
|
|
34
|
+
* @param obj - Any Godot wrapper (Node, Resource, …), value type, or plain value
|
|
35
|
+
*/
|
|
36
|
+
export function dumpStr(obj: unknown): string {
|
|
37
|
+
if (obj == null || typeof obj !== 'object') return String(obj);
|
|
38
|
+
|
|
39
|
+
// Value types & heap containers (Vector3, Array, Dictionary, …) have no getClass() —
|
|
40
|
+
// let Godot's str() render them.
|
|
41
|
+
if (!(obj as any).getClass) return str(obj as any);
|
|
42
|
+
|
|
43
|
+
const dump = varToStr(obj as any);
|
|
44
|
+
let name: unknown;
|
|
45
|
+
try { name = (obj as any).name; } catch { name = undefined; }
|
|
46
|
+
return name === undefined || name === null || name === '' ? dump : `${name}: ${dump}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Serialize a scene subtree as a tree-formatted string, optionally filtered.
|
|
51
|
+
*
|
|
52
|
+
* Emits one line per node: `Class "name" (N children)`. When `pattern` is a
|
|
53
|
+
* string, only lines containing it (case-insensitive) are kept; a `RegExp`
|
|
54
|
+
* matches against the full line.
|
|
55
|
+
*
|
|
56
|
+
* @param pattern - Optional substring or `RegExp` to filter the lines
|
|
57
|
+
* @param node - Root node to start from (defaults to the scene tree's root Window)
|
|
58
|
+
* @param depth - Max depth (default 99)
|
|
59
|
+
*/
|
|
60
|
+
export function snapshot(pattern?: string | RegExp, node?: Node, depth = 99): string {
|
|
61
|
+
const root = node ?? tree.root;
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
(function walk(n: Node, d: number, ind: string) {
|
|
64
|
+
if (d <= 0) return;
|
|
65
|
+
const count = n.getChildCount();
|
|
66
|
+
const line = `${ind}${n.getClass()} "${n.name}" (${count} children)`;
|
|
67
|
+
if (matches(pattern, line)) lines.push(line);
|
|
68
|
+
for (let i = 0; i < count; i++) {
|
|
69
|
+
const c = n.getChild(i);
|
|
70
|
+
if (c) walk(c as Node, d - 1, ind + ' ');
|
|
71
|
+
}
|
|
72
|
+
})(root, depth, '');
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a node from the scene root by absolute NodePath (`/root/...`) or
|
|
78
|
+
* name glob (`findChild`). Returns `null` when not found.
|
|
79
|
+
*/
|
|
80
|
+
function resolveNode(path: string, root: Window): Node | null {
|
|
81
|
+
if (path.startsWith('/')) return root.getNode(path);
|
|
82
|
+
return root.findChild(path, true, false);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve a node by path (absolute NodePath or name glob) from the scene root
|
|
87
|
+
* and dump it via {@link dumpStr}.
|
|
88
|
+
*
|
|
89
|
+
* @param path - An absolute NodePath like `/root/Box`, or a `findChild` name/glob pattern
|
|
90
|
+
*/
|
|
91
|
+
export function inspect(path: string): string | null {
|
|
92
|
+
const node = resolveNode(path, tree.root);
|
|
93
|
+
return node ? dumpStr(node) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Synthesize a left-click at the center of a {@link Control} matched by `path`.
|
|
98
|
+
*
|
|
99
|
+
* Press and release are pushed through `Window.pushInput` (the GUI-routing
|
|
100
|
+
* path — `Input.parseInputEvent` doesn't reliably reach `Control.guiInput` on
|
|
101
|
+
* the wasm build). Positions are in device space (logical × `contentScaleFactor`).
|
|
102
|
+
*
|
|
103
|
+
* @param path - An absolute NodePath like `/root/...`, or a `findChild` name/glob pattern
|
|
104
|
+
* @returns The node's `dumpStr`, or `null` if not found or not a `Control`
|
|
105
|
+
*/
|
|
106
|
+
export function click(path: string): string | null {
|
|
107
|
+
const root = tree.root;
|
|
108
|
+
const node = resolveNode(path, root);
|
|
109
|
+
if (!(node instanceof Control)) return null;
|
|
110
|
+
const rect = node.getGlobalRect();
|
|
111
|
+
const scale = root.contentScaleFactor;
|
|
112
|
+
const position: Vector2 = [
|
|
113
|
+
(rect[0] + rect[2] / 2) * scale,
|
|
114
|
+
(rect[1] + rect[3] / 2) * scale,
|
|
115
|
+
];
|
|
116
|
+
const press = (down: boolean) => {
|
|
117
|
+
const event = new InputEventMouseButton();
|
|
118
|
+
event.position = position;
|
|
119
|
+
event.globalPosition = position;
|
|
120
|
+
event.buttonIndex = MouseButton.MOUSE_BUTTON_LEFT;
|
|
121
|
+
event.pressed = down;
|
|
122
|
+
root.pushInput(event);
|
|
123
|
+
};
|
|
124
|
+
press(true);
|
|
125
|
+
setTimeout(() => press(false), 50);
|
|
126
|
+
return dumpStr(node);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Enumerate nodes in the scene by name pattern and/or class, in tree order.
|
|
131
|
+
*
|
|
132
|
+
* @param pattern - Name glob matched by `findChildren` (default `"*"`)
|
|
133
|
+
* @param type - Class name to filter by, e.g. `"RigidBody3D"` (default `""` = any)
|
|
134
|
+
* @param node - Root node to search under (defaults to the scene tree's root Window)
|
|
135
|
+
*/
|
|
136
|
+
export function find(pattern = '*', type = '', node?: Node): Node[] {
|
|
137
|
+
const root = node ?? tree.root;
|
|
138
|
+
return [...root.findChildren(pattern, type, true, false)];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run `gc()` and report how much heap was freed.
|
|
143
|
+
* Requires the `--expose-gc` Node.js flag.
|
|
144
|
+
*/
|
|
145
|
+
export function gc(): string {
|
|
146
|
+
const before = globalThis.process?.memoryUsage?.().heapUsed;
|
|
147
|
+
if (globalThis.Bun)
|
|
148
|
+
globalThis.Bun.gc(true);
|
|
149
|
+
else if (globalThis.gc)
|
|
150
|
+
globalThis.gc({ type: 'major', execution: 'sync' });
|
|
151
|
+
else
|
|
152
|
+
return 'gc() unavailable — need --expose-gc';
|
|
153
|
+
|
|
154
|
+
const after = globalThis.process?.memoryUsage?.().heapUsed;
|
|
155
|
+
const freed = typeof before === 'number' && typeof after === 'number' ? (before - after) / 1024 : 0;
|
|
156
|
+
return `GC: freed ${freed.toFixed(0)} KB`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* `DOMRect` is not a global in Bun (desktop), but `getBoundingClientRect()`
|
|
161
|
+
* constructs one. Install a minimal spec-compatible polyfill on desktop so the
|
|
162
|
+
* helper works identically on desktop and web (browsers already define it).
|
|
163
|
+
*/
|
|
164
|
+
function ensureDOMRect(): void {
|
|
165
|
+
if (typeof (globalThis as any).DOMRect !== 'undefined') return;
|
|
166
|
+
(globalThis as any).DOMRect = class DOMRect {
|
|
167
|
+
x: number;
|
|
168
|
+
y: number;
|
|
169
|
+
width: number;
|
|
170
|
+
height: number;
|
|
171
|
+
constructor(x = 0, y = 0, width = 0, height = 0) {
|
|
172
|
+
this.x = x;
|
|
173
|
+
this.y = y;
|
|
174
|
+
this.width = width;
|
|
175
|
+
this.height = height;
|
|
176
|
+
}
|
|
177
|
+
get left() { return this.x; }
|
|
178
|
+
get top() { return this.y; }
|
|
179
|
+
get right() { return this.x + this.width; }
|
|
180
|
+
get bottom() { return this.y + this.height; }
|
|
181
|
+
toJSON() { return { x: this.x, y: this.y, width: this.width, height: this.height }; }
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type Push = (x: number, y: number) => void;
|
|
186
|
+
type BoundsCollect = (node: any, push: Push, viewport: Viewport) => void;
|
|
187
|
+
|
|
188
|
+
/** Affine 2D point mapper from a transform's basis/origin, optionally scaled. */
|
|
189
|
+
function makeAffine(t: any, scale = 1): (x: number, y: number) => [number, number] {
|
|
190
|
+
const [tx, ty, to] = t;
|
|
191
|
+
const tox = to[0], toy = to[1];
|
|
192
|
+
const txx = tx[0], txy = tx[1];
|
|
193
|
+
const tyx = ty[0], tyy = ty[1];
|
|
194
|
+
return (x, y) => [(tox + txx * x + tyx * y) * scale, (toy + txy * x + tyy * y) * scale];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Axis-aligned 2D bounds accumulator in CSS pixels. Non-finite points (e.g.
|
|
199
|
+
* geometry behind the camera) are dropped; `rect()` is `null` when no point
|
|
200
|
+
* was added.
|
|
201
|
+
*/
|
|
202
|
+
function cssBounds(): { add(x: number, y: number): void; rect(): [number, number, number, number] | null } {
|
|
203
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
204
|
+
let found = false;
|
|
205
|
+
return {
|
|
206
|
+
add(x, y) {
|
|
207
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
208
|
+
if (x < minX) minX = x; if (y < minY) minY = y;
|
|
209
|
+
if (x > maxX) maxX = x; if (y > maxY) maxY = y;
|
|
210
|
+
found = true;
|
|
211
|
+
},
|
|
212
|
+
rect() {
|
|
213
|
+
return found ? [minX, minY, maxX - minX, maxY - minY] : null;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** World-space AABB (min, max) of a local AABB under its global transform, or `null` when degenerate. */
|
|
219
|
+
function worldAabb(aabb: any, gt: any): [number, number, number, number, number, number] | null {
|
|
220
|
+
const [position, size] = aabb;
|
|
221
|
+
const [bx, by, bz, origin] = gt;
|
|
222
|
+
if (size[0] === 0 && size[1] === 0 && size[2] === 0) return null;
|
|
223
|
+
const cx = position[0] + size[0] / 2;
|
|
224
|
+
const cy = position[1] + size[1] / 2;
|
|
225
|
+
const cz = position[2] + size[2] / 2;
|
|
226
|
+
const ex = Math.hypot(bx[0], bx[1], bx[2]) * size[0] / 2;
|
|
227
|
+
const ey = Math.hypot(by[0], by[1], by[2]) * size[1] / 2;
|
|
228
|
+
const ez = Math.hypot(bz[0], bz[1], bz[2]) * size[2] / 2;
|
|
229
|
+
const ox = origin[0], oy = origin[1], oz = origin[2];
|
|
230
|
+
const wx = ox + bx[0] * cx + by[0] * cy + bz[0] * cz;
|
|
231
|
+
const wy = oy + bx[1] * cx + by[1] * cy + bz[1] * cz;
|
|
232
|
+
const wz = oz + bx[2] * cx + by[2] * cy + bz[2] * cz;
|
|
233
|
+
return [wx - ex, wy - ey, wz - ez, wx + ex, wy + ey, wz + ez];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* World-space AABB of a node's visual content: the union of its own AABB (when
|
|
238
|
+
* it is a `VisualInstance3D`) and every descendant `VisualInstance3D`'s, each in
|
|
239
|
+
* its own global transform — mirrors the editor's `_calculate_spatial_bounds`.
|
|
240
|
+
* Returns `null` when there is no non-degenerate visual.
|
|
241
|
+
*/
|
|
242
|
+
function worldBounds(node: Node3D): [number, number, number, number, number, number] | null {
|
|
243
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
244
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
245
|
+
const add = (aabb: any, gt: any) => {
|
|
246
|
+
const box = worldAabb(aabb, gt);
|
|
247
|
+
if (!box) return;
|
|
248
|
+
if (box[0] < minX) minX = box[0]; if (box[3] > maxX) maxX = box[3];
|
|
249
|
+
if (box[1] < minY) minY = box[1]; if (box[4] > maxY) maxY = box[4];
|
|
250
|
+
if (box[2] < minZ) minZ = box[2]; if (box[5] > maxZ) maxZ = box[5];
|
|
251
|
+
};
|
|
252
|
+
if (typeof (node as any).getAabb === 'function') {
|
|
253
|
+
add((node as any).getAabb(), node.globalTransform);
|
|
254
|
+
}
|
|
255
|
+
for (const child of node.findChildren('*', 'VisualInstance3D', true, false) as unknown as any[]) {
|
|
256
|
+
if (child && typeof child.getAabb === 'function') {
|
|
257
|
+
add(child.getAabb(), child.globalTransform);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return minX === Infinity ? null : [minX, minY, minZ, maxX, maxY, maxZ];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Compute a node's bounding rect in CSS pixels. The callback pushes points in
|
|
265
|
+
* viewport logical units; they're mapped through the viewport's screen transform
|
|
266
|
+
* (content scale, stretch, SubViewport/child-window placement), then divided by
|
|
267
|
+
* screenGetScale to reach CSS pixels. Never throws: React DevTools calls this on
|
|
268
|
+
* every commit and must not see a throw (e.g. a wrapper freed by an unmount).
|
|
269
|
+
*/
|
|
270
|
+
function boundsForViewport(node: Node, collect: (push: Push, viewport: Viewport) => void): DOMRect | null {
|
|
271
|
+
try {
|
|
272
|
+
const viewport = node.getViewport();
|
|
273
|
+
if (!viewport) return null;
|
|
274
|
+
const xform = makeAffine(viewport.getScreenTransform(), 1 / DisplayServer.screenGetScale());
|
|
275
|
+
const bounds = cssBounds();
|
|
276
|
+
collect((x, y) => bounds.add(...xform(x, y)), viewport);
|
|
277
|
+
const r = bounds.rect();
|
|
278
|
+
return r ? new DOMRect(r[0], r[1], r[2], r[3]) : null;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Install `getBoundingClientRect` on a node prototype. */
|
|
285
|
+
function defineBoundingClientRect(proto: object, collect: BoundsCollect): void {
|
|
286
|
+
Object.defineProperty(proto, 'getBoundingClientRect', {
|
|
287
|
+
configurable: true,
|
|
288
|
+
value(this: Node): DOMRect | null {
|
|
289
|
+
return boundsForViewport(this, (push, viewport) => collect(this, push, viewport));
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Push the four corners of an axis-aligned rect through `emit`. */
|
|
295
|
+
function pushRect(emit: (x: number, y: number) => void, x0: number, y0: number, x1: number, y1: number): void {
|
|
296
|
+
for (const px of [x0, x1]) {
|
|
297
|
+
for (const py of [y0, y1]) {
|
|
298
|
+
emit(px, py);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const canvasItemBounds: BoundsCollect = (node, push) => {
|
|
304
|
+
if (node instanceof Control) {
|
|
305
|
+
const [x, y, w, h] = node.getGlobalRect();
|
|
306
|
+
pushRect(push, x, y, x + w, y + h);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (typeof node.getRect !== 'function') return;
|
|
310
|
+
const [x, y, w, h] = node.getRect() as [number, number, number, number];
|
|
311
|
+
const xform = makeAffine(node.getScreenTransform());
|
|
312
|
+
pushRect((px, py) => push(...xform(px, py)), x, y, x + w, y + h);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const node3dBounds: BoundsCollect = (node, push, viewport) => {
|
|
316
|
+
const camera = viewport.getCamera3d();
|
|
317
|
+
if (!camera) return;
|
|
318
|
+
const box = worldBounds(node);
|
|
319
|
+
if (!box) return;
|
|
320
|
+
const [minX, minY, minZ, maxX, maxY, maxZ] = box;
|
|
321
|
+
for (let i = 0; i < 8; i++) {
|
|
322
|
+
const s = camera.unprojectPosition([
|
|
323
|
+
i & 1 ? maxX : minX,
|
|
324
|
+
i & 2 ? maxY : minY,
|
|
325
|
+
i & 4 ? maxZ : minZ,
|
|
326
|
+
]);
|
|
327
|
+
push(s[0], s[1]);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Bootstrap the debugging environment.
|
|
333
|
+
*
|
|
334
|
+
* - Exposes common singletons (`Engine`, `OS`, `ClassDB`, `Time`) and all
|
|
335
|
+
* debug utility functions on `globalThis.$` so they're reachable from
|
|
336
|
+
* breakpoints in any module.
|
|
337
|
+
* - Registers an `uncaughtException` handler that logs but keeps the process
|
|
338
|
+
* alive for interactive debugging.
|
|
339
|
+
* - Polyfills `DOMRect` (needed by `getBoundingClientRect()` on desktop).
|
|
340
|
+
*/
|
|
341
|
+
export function initDebug(): void {
|
|
342
|
+
globalThis.process?.on?.('uncaughtException', (err, origin) => {
|
|
343
|
+
console.error(`\n✗ ${origin}:`, err.stack);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
ensureDOMRect();
|
|
347
|
+
defineBoundingClientRect(CanvasItem.prototype, canvasItemBounds);
|
|
348
|
+
defineBoundingClientRect(Node3D.prototype, node3dBounds);
|
|
349
|
+
|
|
350
|
+
(globalThis as any).$ = {
|
|
351
|
+
Engine, OS, ClassDB, Time,
|
|
352
|
+
dumpStr, snapshot, inspect, click, find, gc,
|
|
353
|
+
get tree(): SceneTree { return tree; },
|
|
354
|
+
get paused(): boolean { return tree.paused; },
|
|
355
|
+
set paused(v: boolean) { tree.paused = v; },
|
|
356
|
+
};
|
|
357
|
+
}
|