@downdraft/engine 0.1.2-alpha.5 → 0.3.0
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/app/src/renderer/bootstrap.ts +1 -1
- package/app/src/renderer/feature-log.ts +1 -1
- package/core/src/index.ts +2 -2
- package/core/src/input/local-player-manager.ts +2 -2
- package/core/src/platform/rpc.ts +4 -1
- package/core/src/render/game-renderer.ts +127 -64
- package/core/src/sab/grid-buffer.ts +55 -2
- package/core/src/sab/record-buffer.ts +48 -5
- package/core/src/sab/sim-channel.ts +181 -17
- package/core/src/sab/slot-buffer.ts +55 -1
- package/core/src/sab/types.ts +11 -0
- package/core/src/telemetry/gpu-resource-tracker.ts +24 -0
- package/core/src/util/index.ts +1 -0
- package/core/src/util/logger.ts +2 -2
- package/core/src/util/session-timer.ts +23 -0
- package/core/src/worker/base-worker-host.ts +2 -1
- package/core/src/worker/buffer-sync.ts +20 -13
- package/core/src/worker/rpc.ts +54 -5
- package/core/src/worker/sim-worker-base.ts +6 -3
- package/core/src/worker/task-pool.ts +4 -0
- package/dist/.dd-manifest.json +879 -869
- package/dist/app/src/renderer/bootstrap.js +1 -1
- package/dist/app/src/renderer/feature-log.js +1 -1
- package/dist/core/src/index.js +2 -2
- package/dist/core/src/input/local-player-manager.js +2 -2
- package/dist/core/src/platform/rpc.js +2 -1
- package/dist/core/src/render/game-renderer.js +125 -59
- package/dist/core/src/sab/grid-buffer.js +53 -1
- package/dist/core/src/sab/record-buffer.js +49 -5
- package/dist/core/src/sab/sim-channel.js +173 -17
- package/dist/core/src/sab/slot-buffer.js +54 -1
- package/dist/core/src/telemetry/gpu-resource-tracker.js +26 -0
- package/dist/core/src/util/index.js +1 -0
- package/dist/core/src/util/logger.js +2 -2
- package/dist/core/src/util/session-timer.js +22 -0
- package/dist/core/src/worker/base-worker-host.js +2 -1
- package/dist/core/src/worker/buffer-sync.js +16 -9
- package/dist/core/src/worker/rpc.js +46 -8
- package/dist/core/src/worker/sim-worker-base.js +6 -3
- package/dist/core/src/worker/task-pool.js +5 -0
- package/dist/libraries/devtools/src/backend.js +35 -9
- package/dist/libraries/devtools/src/collectors.js +73 -22
- package/dist/libraries/gamepad/src/feed-input.js +1 -1
- package/dist/libraries/gamepad/src/hub.js +2 -2
- package/dist/libraries/gamepad/src/source-sab.js +2 -2
- package/dist/libraries/gamepad/src/sysfs-enrich.js +2 -2
- package/dist/libraries/gpu-kernels/src/kernel.js +1 -1
- package/dist/libraries/gpu-kernels/src/transpile.js +1 -1
- package/dist/libraries/html-ui-kit/src/nav/nav-controller.js +1 -1
- package/dist/libraries/html-ui-kit/src/nav/nav-input.js +2 -2
- package/dist/libraries/networking/src/discovery.js +8 -8
- package/dist/libraries/persistence/src/file-save-store.js +5 -5
- package/dist/libraries/persistence/src/opfs-save-store.js +2 -2
- package/dist/modules/controller-ui/src/index.js +1 -1
- package/dist/modules/devtools/src/blitz/fonts.js +75 -0
- package/dist/modules/devtools/src/blitz/host.js +127 -33
- package/dist/modules/devtools/src/blitz/panels/console.js +19 -7
- package/dist/modules/devtools/src/blitz/panels/elements.js +217 -28
- package/dist/modules/devtools/src/blitz/panels/gpu.js +5 -2
- package/dist/modules/devtools/src/blitz/panels/performance.js +28 -14
- package/dist/modules/devtools/src/blitz/panels/snapshot.js +31 -8
- package/dist/modules/devtools/src/blitz/panels/types.js +2 -1
- package/dist/modules/devtools/src/blitz/theme.js +26 -4
- package/dist/modules/html-ui/src/doc-backend.js +52 -3
- package/dist/modules/html-ui/src/host.js +6 -3
- package/dist/modules/html-ui/src/jsx-runtime.js +1 -1
- package/libraries/blitz-ui/native/Cargo.toml +6 -0
- package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
- package/libraries/devtools/src/backend.ts +38 -12
- package/libraries/devtools/src/collectors.ts +75 -22
- package/libraries/gamepad/src/feed-input.ts +1 -1
- package/libraries/gamepad/src/hub.ts +2 -2
- package/libraries/gamepad/src/source-sab.ts +2 -2
- package/libraries/gamepad/src/sysfs-enrich.ts +2 -2
- package/libraries/gpu-kernels/src/kernel.ts +2 -2
- package/libraries/gpu-kernels/src/transpile.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-controller.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-input.ts +2 -2
- package/libraries/networking/src/discovery.ts +8 -8
- package/libraries/persistence/src/file-save-store.ts +5 -5
- package/libraries/persistence/src/opfs-save-store.ts +2 -2
- package/libraries/physics-rapier/src/realm-worker-script.ts +1 -1
- package/modules/controller-ui/src/index.ts +1 -1
- package/modules/devtools/src/blitz/fonts.ts +78 -0
- package/modules/devtools/src/blitz/host.ts +110 -34
- package/modules/devtools/src/blitz/panels/console.ts +20 -8
- package/modules/devtools/src/blitz/panels/elements.ts +212 -31
- package/modules/devtools/src/blitz/panels/gpu.ts +4 -2
- package/modules/devtools/src/blitz/panels/performance.ts +26 -14
- package/modules/devtools/src/blitz/panels/snapshot.ts +30 -8
- package/modules/devtools/src/blitz/panels/types.ts +2 -1
- package/modules/devtools/src/blitz/theme.ts +26 -4
- package/modules/html-ui/src/doc-backend.ts +67 -4
- package/modules/html-ui/src/host.ts +11 -3
- package/modules/html-ui/src/jsx-runtime.ts +1 -1
- package/modules/html-ui/src/protocol.ts +1 -1
- package/package.json +2 -2
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
import { createLogger } from "@downdraft/engine";
|
|
14
14
|
import { CdpBridge, DevtoolsBackend } from "@downdraft/engine/libraries/devtools";
|
|
15
15
|
import { HtmlUiHost } from "@downdraft/engine/modules/html-ui";
|
|
16
|
+
import { registerDevtoolsFonts } from "./fonts.js";
|
|
16
17
|
import { ConsolePanel } from "./panels/console.js";
|
|
17
18
|
import { ElementsPanel } from "./panels/elements.js";
|
|
18
19
|
import { GpuPanel } from "./panels/gpu.js";
|
|
19
20
|
import { PerformancePanel } from "./panels/performance.js";
|
|
20
21
|
import { SnapshotPanel } from "./panels/snapshot.js";
|
|
22
|
+
import { esc } from "./panels/types.js";
|
|
21
23
|
import { DEVTOOLS_CSS } from "./theme.js";
|
|
22
24
|
const log = createLogger("info");
|
|
23
25
|
const MIN_DOCK = 120;
|
|
@@ -42,6 +44,9 @@ export class BlitzDevtoolsHost {
|
|
|
42
44
|
panelCtx;
|
|
43
45
|
lastPanelRender = -1e9;
|
|
44
46
|
lastSlowLog = 0;
|
|
47
|
+
/** @font-face css from registerDevtoolsFonts — "" means "embedded font
|
|
48
|
+
* fallback", null means "not yet attempted". */
|
|
49
|
+
fontCss = null;
|
|
45
50
|
constructor(opts) {
|
|
46
51
|
this.opts = opts;
|
|
47
52
|
this.surfaceW = opts.surface.clientWidth;
|
|
@@ -76,7 +81,7 @@ export class BlitzDevtoolsHost {
|
|
|
76
81
|
new ElementsPanel(ctx),
|
|
77
82
|
new PerformancePanel(ctx),
|
|
78
83
|
new GpuPanel(ctx),
|
|
79
|
-
])
|
|
84
|
+
].values())
|
|
80
85
|
this.panels.set(p.id, p);
|
|
81
86
|
}
|
|
82
87
|
get visible() { return this.panel !== null; }
|
|
@@ -113,19 +118,42 @@ export class BlitzDevtoolsHost {
|
|
|
113
118
|
show() {
|
|
114
119
|
if (!this._ready || this.visible || this.disposed)
|
|
115
120
|
return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
// Register real system fonts once per host — ui:// resources persist on
|
|
122
|
+
// the surface across dock remounts, and must land before the doc mounts
|
|
123
|
+
// so the first resolve sees them (embedded DejaVu fallback otherwise).
|
|
124
|
+
if (this.fontCss === null) {
|
|
125
|
+
const ui = this.opts.ui;
|
|
126
|
+
const rr = ui.registerResource?.bind(ui)
|
|
127
|
+
?? (ui.host ? ui.host.registerResource.bind(ui.host) : undefined);
|
|
128
|
+
this.fontCss = rr ? registerDevtoolsFonts({ registerResource: rr }) : "";
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
this.panel = this.opts.ui.mount(this.shellHtml(), {
|
|
132
|
+
id: "devtools",
|
|
133
|
+
rect: this.dockRect(),
|
|
134
|
+
// Above game overlays — menus/loading screens shouldn't bury the dock.
|
|
135
|
+
z: 1000,
|
|
136
|
+
// Native-res raster: the vendored glifo/FreeType path grid-fits hinted
|
|
137
|
+
// glyph stems to whole pixels — supersample factors >1 just blur that
|
|
138
|
+
// back out on downscale (and cost 2.25x the texture memory).
|
|
139
|
+
scale: 1,
|
|
140
|
+
// Ease wheel detents at pump cadence instead of jumping per event.
|
|
141
|
+
smoothWheel: true,
|
|
142
|
+
interactive: true,
|
|
143
|
+
// Devtools UI doesn't need vsync-rate raster — cap it so mutation
|
|
144
|
+
// bursts (console streams, snapshot refreshes) coalesce into ~66ms
|
|
145
|
+
// rasters instead of one raster per message.
|
|
146
|
+
maxFps: 15,
|
|
147
|
+
onEvent: (ev) => this.handleDocEvent(ev),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
// Mount fails when the ui worker is dead — stay hidden so the next F12
|
|
152
|
+
// retries cleanly instead of wedging on a half-mounted doc.
|
|
153
|
+
this.panel = null;
|
|
154
|
+
log.error("blitz-devtools-host", `dock mount failed: ${err}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
129
157
|
this.syncProviders();
|
|
130
158
|
this.renderTabs();
|
|
131
159
|
this.activate(this.activeId);
|
|
@@ -150,8 +178,19 @@ export class BlitzDevtoolsHost {
|
|
|
150
178
|
const t1 = performance.now();
|
|
151
179
|
if (!this.visible)
|
|
152
180
|
return;
|
|
153
|
-
|
|
154
|
-
this.
|
|
181
|
+
// Guarded like backend.update() above — a throwing panel render can't be
|
|
182
|
+
// allowed to kill the game frame hook that drives this pump.
|
|
183
|
+
try {
|
|
184
|
+
this.syncProviders();
|
|
185
|
+
this.flush();
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
const tn = performance.now();
|
|
189
|
+
if (tn - this.lastSlowLog > 1000) {
|
|
190
|
+
this.lastSlowLog = tn;
|
|
191
|
+
log.warn("blitz-devtools-host", `panel render error: ${err}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
155
194
|
const t2 = performance.now();
|
|
156
195
|
// ≤1/sec — these diagnostics used to feed the console panel's logger
|
|
157
196
|
// sink, and each row forced another dock re-render: a self-feeding
|
|
@@ -204,11 +243,13 @@ export class BlitzDevtoolsHost {
|
|
|
204
243
|
}
|
|
205
244
|
// ── Dock layout ──
|
|
206
245
|
dockRect() {
|
|
207
|
-
|
|
208
|
-
|
|
246
|
+
// A tiny surface (or a pathological resize event) must not produce a
|
|
247
|
+
// negative height — keep at least enough room for the tab strip.
|
|
248
|
+
const h = Math.max(48, Math.min(this.dockH, this.surfaceH - 60));
|
|
249
|
+
return { x: 0, y: Math.max(0, this.surfaceH - h), w: this.surfaceW, h };
|
|
209
250
|
}
|
|
210
251
|
shellHtml() {
|
|
211
|
-
return `<html><head><style>${DEVTOOLS_CSS}</style></head><body>
|
|
252
|
+
return `<html><head><style>${this.fontCss ?? ""}${DEVTOOLS_CSS}</style></head><body>
|
|
212
253
|
<div id="dt-root">
|
|
213
254
|
<div id="dt-grip" title="drag to resize"></div>
|
|
214
255
|
<div id="dt-chrome">
|
|
@@ -224,7 +265,10 @@ export class BlitzDevtoolsHost {
|
|
|
224
265
|
renderTabs() {
|
|
225
266
|
if (!this.panel)
|
|
226
267
|
return;
|
|
227
|
-
|
|
268
|
+
// esc() both interpolations — provider names feed panel ids/titles and
|
|
269
|
+
// arrive as raw strings (a game registering "x\"><img" would otherwise
|
|
270
|
+
// inject markup into the tab strip).
|
|
271
|
+
const tabs = [...this.panels.values()].map((p) => `<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${esc(p.id)}">${esc(p.title)}</span>`).join("");
|
|
228
272
|
this.panel.setInnerHtml("#dt-tabs", `<span class="brand">downdraft</span>${tabs}<span class="spacer"></span>
|
|
229
273
|
<span class="hint">docked · F12</span><span id="dt-close" data-action="dt.close">✕</span>`);
|
|
230
274
|
this.tabsDirty = false;
|
|
@@ -235,13 +279,28 @@ export class BlitzDevtoolsHost {
|
|
|
235
279
|
this.activeId = id;
|
|
236
280
|
const p = this.panels.get(id);
|
|
237
281
|
this.renderTabs();
|
|
238
|
-
|
|
239
|
-
|
|
282
|
+
try {
|
|
283
|
+
this.panel.setInnerHtml("#dt-top", p.renderTop?.() ?? "");
|
|
284
|
+
this.panel.setInnerHtml("#dt-bottom", p.renderBottom?.() ?? "");
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
log.warn("blitz-devtools-host", `panel ${id} chrome render error: ${err}`);
|
|
288
|
+
}
|
|
240
289
|
p.dirty = true;
|
|
241
290
|
p.shellDirty = false;
|
|
242
|
-
this.panel.setInnerHtml("#dt-body",
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
this.panel.setInnerHtml("#dt-body", this.safeBody(p));
|
|
292
|
+
try {
|
|
293
|
+
p.afterRender?.();
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
log.warn("blitz-devtools-host", `panel ${id} afterRender error: ${err}`);
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
p.activate?.();
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
log.warn("blitz-devtools-host", `panel ${id} activate error: ${err}`);
|
|
303
|
+
}
|
|
245
304
|
// Only the snapshot tab being viewed gets periodic provider refreshes;
|
|
246
305
|
// fixed panels suspend auto-refresh entirely (manual Refresh still works).
|
|
247
306
|
this.backend.providerWatch = p.providerSlot ?? -1;
|
|
@@ -252,7 +311,7 @@ export class BlitzDevtoolsHost {
|
|
|
252
311
|
const slots = this.backend.providerSlots();
|
|
253
312
|
let changed = false;
|
|
254
313
|
const seen = new Set();
|
|
255
|
-
for (const { slot, name } of slots) {
|
|
314
|
+
for (const { slot, name } of slots.values()) {
|
|
256
315
|
seen.add(slot);
|
|
257
316
|
if (!this.snapshotPanels.has(slot)) {
|
|
258
317
|
const p = new SnapshotPanel(this.panelCtx, slot, name);
|
|
@@ -261,7 +320,7 @@ export class BlitzDevtoolsHost {
|
|
|
261
320
|
changed = true;
|
|
262
321
|
}
|
|
263
322
|
}
|
|
264
|
-
for (const [slot, p] of this.snapshotPanels) {
|
|
323
|
+
for (const [slot, p] of this.snapshotPanels.entries()) {
|
|
265
324
|
if (!seen.has(slot)) {
|
|
266
325
|
this.snapshotPanels.delete(slot);
|
|
267
326
|
this.panels.delete(p.id);
|
|
@@ -307,10 +366,32 @@ export class BlitzDevtoolsHost {
|
|
|
307
366
|
}
|
|
308
367
|
if (p.dirty) {
|
|
309
368
|
p.dirty = false;
|
|
310
|
-
|
|
311
|
-
|
|
369
|
+
let handled = false;
|
|
370
|
+
try {
|
|
371
|
+
handled = p.flushDom?.() === true;
|
|
312
372
|
}
|
|
313
|
-
|
|
373
|
+
catch (err) {
|
|
374
|
+
log.warn("blitz-devtools-host", `panel ${p.id} flushDom error: ${err}`);
|
|
375
|
+
}
|
|
376
|
+
if (!handled)
|
|
377
|
+
this.panel.setInnerHtml("#dt-body", this.safeBody(p));
|
|
378
|
+
try {
|
|
379
|
+
p.afterRender?.();
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
log.warn("blitz-devtools-host", `panel ${p.id} afterRender error: ${err}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** renderBody that can't take the dock down — the error surfaces in-panel
|
|
387
|
+
* (and the next push retries since dirty just flips back on). */
|
|
388
|
+
safeBody(p) {
|
|
389
|
+
try {
|
|
390
|
+
return p.renderBody();
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
log.warn("blitz-devtools-host", `panel ${p.id} renderBody error: ${err}`);
|
|
394
|
+
return `<div class="status-banner error">panel render error: ${esc(String(err))}</div>`;
|
|
314
395
|
}
|
|
315
396
|
}
|
|
316
397
|
setStatus(text) {
|
|
@@ -345,8 +426,10 @@ export class BlitzDevtoolsHost {
|
|
|
345
426
|
return;
|
|
346
427
|
}
|
|
347
428
|
}
|
|
429
|
+
// Left-click only — routing actions on contextmenu meant a right-click
|
|
430
|
+
// on a tab/button fired it instead of just focusing.
|
|
348
431
|
const action = ev.d?.action;
|
|
349
|
-
if (
|
|
432
|
+
if (ev.t === "click" && action) {
|
|
350
433
|
if (action === "dt.tab") {
|
|
351
434
|
const id = ev.d.tab;
|
|
352
435
|
if (id)
|
|
@@ -357,11 +440,21 @@ export class BlitzDevtoolsHost {
|
|
|
357
440
|
this.hide();
|
|
358
441
|
return;
|
|
359
442
|
}
|
|
360
|
-
|
|
443
|
+
try {
|
|
444
|
+
this.panels.get(this.activeId)?.onAction?.(ev.d, ev);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
log.warn("blitz-devtools-host", `panel action error: ${err}`);
|
|
448
|
+
}
|
|
361
449
|
return;
|
|
362
450
|
}
|
|
363
451
|
// Everything else (input, keydown/up, scroll, focus…) → active panel.
|
|
364
|
-
|
|
452
|
+
try {
|
|
453
|
+
this.panels.get(this.activeId)?.onEvent?.(ev);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
log.warn("blitz-devtools-host", `panel event error: ${err}`);
|
|
457
|
+
}
|
|
365
458
|
}
|
|
366
459
|
}
|
|
367
460
|
/**
|
|
@@ -387,5 +480,6 @@ export function createSelfHostedDevtoolsUi(ctx) {
|
|
|
387
480
|
host,
|
|
388
481
|
mount: (markup, spec) => host.mount({ ...spec, html: markup }),
|
|
389
482
|
docCaps: () => host.docCaps,
|
|
483
|
+
registerResource: (url, bytes) => host.registerResource(url, bytes),
|
|
390
484
|
};
|
|
391
485
|
}
|
|
@@ -101,7 +101,7 @@ export class ConsolePanel {
|
|
|
101
101
|
onBackendEvent(event, data) {
|
|
102
102
|
if (event === "console") {
|
|
103
103
|
const e = data;
|
|
104
|
-
this.rows.push({ text: e
|
|
104
|
+
this.rows.push({ text: String(e?.text ?? ""), severity: e?.severity ?? 0, thread: e?.thread ?? "main", seq: this.seq++ });
|
|
105
105
|
if (this.rows.length > MAX_ROWS)
|
|
106
106
|
this.rows.splice(0, this.rows.length - MAX_ROWS);
|
|
107
107
|
this.dirty = true;
|
|
@@ -113,15 +113,24 @@ export class ConsolePanel {
|
|
|
113
113
|
}
|
|
114
114
|
else if (event === "threads") {
|
|
115
115
|
const list = data ?? [];
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const next = [{ id: "main", name: "main", kind: 0 }, ...list.filter((t) => t.id !== "main")];
|
|
117
|
+
// Thread chips live in the bottom chrome — flag it so the row
|
|
118
|
+
// actually re-renders when a worker appears or disappears.
|
|
119
|
+
const changed = next.length !== this.threads.length
|
|
120
|
+
|| next.some((t, i) => t.id !== this.threads[i].id || t.name !== this.threads[i].name);
|
|
121
|
+
this.threads = next;
|
|
122
|
+
if (!this.threads.some((t) => t.id === this.replThread)) {
|
|
118
123
|
this.replThread = "main";
|
|
124
|
+
this.shellDirty = true;
|
|
125
|
+
}
|
|
126
|
+
if (changed)
|
|
127
|
+
this.shellDirty = true;
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
onAction(data, _ev) {
|
|
122
131
|
const act = data.action;
|
|
123
132
|
if (act === "console.clear") {
|
|
124
|
-
void this.ctx.call("console.clear");
|
|
133
|
+
void this.ctx.call("console.clear").catch(() => { });
|
|
125
134
|
}
|
|
126
135
|
else if (act === "console.sev") {
|
|
127
136
|
this.minSev = (this.minSev + 1) % SEV_NAMES.length;
|
|
@@ -171,8 +180,11 @@ export class ConsolePanel {
|
|
|
171
180
|
this.replValue = this.history[--this.histIdx];
|
|
172
181
|
this.ctx.setValue("[data-dt=repl]", this.replValue);
|
|
173
182
|
}
|
|
174
|
-
else if (ev.k === "ArrowDown" && this.histIdx < this.history.length
|
|
175
|
-
|
|
183
|
+
else if (ev.k === "ArrowDown" && this.histIdx >= 0 && this.histIdx < this.history.length) {
|
|
184
|
+
// Stepping past the newest entry restores the empty input —
|
|
185
|
+
// previously the last history item was unreachable-to-leave.
|
|
186
|
+
this.histIdx++;
|
|
187
|
+
this.replValue = this.histIdx < this.history.length ? this.history[this.histIdx] : "";
|
|
176
188
|
this.ctx.setValue("[data-dt=repl]", this.replValue);
|
|
177
189
|
}
|
|
178
190
|
}
|
|
@@ -205,7 +217,7 @@ export class ConsolePanel {
|
|
|
205
217
|
else {
|
|
206
218
|
const r = res?.result;
|
|
207
219
|
this.addRow({
|
|
208
|
-
text: typeof r === "string" ? r : JSON.stringify(r, null, 2),
|
|
220
|
+
text: typeof r === "string" ? r : (r === undefined ? "undefined" : JSON.stringify(r, null, 2)),
|
|
209
221
|
severity: 0, thread: this.replThread,
|
|
210
222
|
});
|
|
211
223
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// panels/elements.ts — Elements panel: scene graph /
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// panels/elements.ts — Elements panel: scene graph / ECS entity tree.
|
|
3
|
+
//
|
|
4
|
+
// Clicking a row selects it (Chrome-style): the .sel class flips via
|
|
5
|
+
// incremental attr mutation so the scrollport survives, and a details strip
|
|
6
|
+
// in the bottom chrome shows the row fields plus `getNodeJSON` output when
|
|
7
|
+
// the node has a stable id and the scene inspector is live. The caret on
|
|
8
|
+
// parent rows folds/unfolds the subtree.
|
|
5
9
|
// ============================================================================
|
|
6
10
|
import { esc } from "./types.js";
|
|
7
|
-
const MODE_LABEL = { scene: "Scene graph",
|
|
11
|
+
const MODE_LABEL = { scene: "Scene graph", ecs: "ECS entities" };
|
|
8
12
|
const MAX_ROWS = 4000;
|
|
9
13
|
export class ElementsPanel {
|
|
10
14
|
id = "elements";
|
|
@@ -14,66 +18,251 @@ export class ElementsPanel {
|
|
|
14
18
|
ctx;
|
|
15
19
|
mode = "scene";
|
|
16
20
|
rows = [];
|
|
21
|
+
/** Stable per-row key (nodeId or label path), parallel to rows. */
|
|
22
|
+
keys = [];
|
|
23
|
+
collapsed = new Set();
|
|
24
|
+
/** Logical selection — survives refreshes via keys. */
|
|
25
|
+
selKey = null;
|
|
26
|
+
/** Ephemeral DOM id of the row currently carrying .sel — lets pick flip
|
|
27
|
+
* the class without a body reparse. Stale after a refresh; harmless —
|
|
28
|
+
* renderBody re-emits .sel from selKey. */
|
|
29
|
+
selRowId = null;
|
|
30
|
+
details = null;
|
|
31
|
+
lastError = "";
|
|
32
|
+
lastSig = "";
|
|
33
|
+
/** Scroll offset of #dt-body — restored after every body re-render, since
|
|
34
|
+
* setInnerHtml replaces the subtree and drops scrollTop. */
|
|
35
|
+
lastScrollTop = 0;
|
|
17
36
|
constructor(ctx) { this.ctx = ctx; }
|
|
18
37
|
renderTop() {
|
|
19
38
|
const btn = (m) => `<span class="dbtn${this.mode === m ? " on" : ""}" data-action="el.mode" data-mode="${m}">${MODE_LABEL[m]}</span>`;
|
|
20
39
|
return `<div class="panel-toolbar">
|
|
21
40
|
<span class="dbtn" data-action="el.refresh">Refresh</span>
|
|
22
|
-
${btn("scene")}${btn("
|
|
41
|
+
${btn("scene")}${btn("ecs")}
|
|
23
42
|
<span class="spacer"></span>
|
|
24
|
-
<span class="count">${this.rows.length} nodes</span>
|
|
43
|
+
<span class="count">${this.rows.length} nodes${this.collapsed.size ? ` · ${this.collapsed.size} folded` : ""}</span>
|
|
25
44
|
</div>`;
|
|
26
45
|
}
|
|
27
46
|
renderBody() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
47
|
+
const err = this.lastError
|
|
48
|
+
? `<div class="status-banner error">${esc(this.lastError)}</div>` : "";
|
|
49
|
+
const vis = this.visibleRows();
|
|
50
|
+
if (!vis.length)
|
|
51
|
+
return err + `<div class="empty-note">no nodes</div>`;
|
|
52
|
+
const html = vis.slice(0, MAX_ROWS).map(({ row: n, key }) => {
|
|
53
|
+
const isSel = key === this.selKey;
|
|
54
|
+
const folded = this.collapsed.has(key);
|
|
55
|
+
const caret = n.childCount > 0
|
|
56
|
+
? `<span class="caret" data-action="el.fold" data-node="${n.id}">${folded ? "▸" : "▾"}</span>`
|
|
57
|
+
: `<span class="caret leaf"></span>`;
|
|
58
|
+
return `<div class="trow${isSel ? " sel" : ""}" style="padding-left:${4 + n.depth * 14}px" data-action="el.pick" data-node="${n.id}">
|
|
59
|
+
${caret}<span>${esc(n.label)}</span>
|
|
32
60
|
${n.detail ? `<span class="detail">${esc(n.detail)}</span>` : ""}
|
|
33
61
|
${n.childCount > 0 ? `<span class="kids">(${n.childCount})</span>` : ""}
|
|
34
|
-
</div
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
</div>`;
|
|
63
|
+
}).join("");
|
|
64
|
+
const more = vis.length > MAX_ROWS
|
|
65
|
+
? `<div class="empty-note">… ${vis.length - MAX_ROWS} more rows</div>` : "";
|
|
66
|
+
return err + `<div class="tree">${html}${more}</div>`;
|
|
38
67
|
}
|
|
68
|
+
renderBottom() {
|
|
69
|
+
if (!this.details)
|
|
70
|
+
return "";
|
|
71
|
+
return `<div class="el-details">
|
|
72
|
+
<div class="el-details-head"><span class="dt-title">${esc(this.details.title)}</span>
|
|
73
|
+
<span class="spacer"></span><span class="dbtn" data-action="el.unpick">✕</span></div>
|
|
74
|
+
<div class="el-details-body lines">${esc(this.details.body)}</div>
|
|
75
|
+
</div>`;
|
|
76
|
+
}
|
|
77
|
+
// ── Data ──
|
|
78
|
+
/** Cheap change-detection — scene/ecs pushes arrive every ~2s while the
|
|
79
|
+
* dock is visible; identical payloads must not force a re-render (it
|
|
80
|
+
* would bounce the scrollport twice per second). */
|
|
81
|
+
applyRows(rows) {
|
|
82
|
+
const sig = JSON.stringify(rows.map((r) => [r.label, r.detail, r.childCount, r.depth, r.nodeId ?? ""]));
|
|
83
|
+
if (sig === this.lastSig)
|
|
84
|
+
return;
|
|
85
|
+
this.lastSig = sig;
|
|
86
|
+
this.setRows(rows);
|
|
87
|
+
this.dirty = true;
|
|
88
|
+
this.shellDirty = true;
|
|
89
|
+
}
|
|
90
|
+
setRows(rows) {
|
|
91
|
+
this.rows = rows;
|
|
92
|
+
// Rows arrive pre-order flattened — the parent of a row is the most
|
|
93
|
+
// recent row at depth-1. Keys: nodeId when the collector provided one,
|
|
94
|
+
// else a positional label path (stable while the tree shape holds).
|
|
95
|
+
const stack = [];
|
|
96
|
+
this.keys = rows.map((r, i) => {
|
|
97
|
+
const parent = r.depth > 0 ? (stack[r.depth - 1] ?? "") : "";
|
|
98
|
+
const k = r.nodeId ?? `${parent}/${r.label}@${i}`;
|
|
99
|
+
stack[r.depth] = k;
|
|
100
|
+
stack.length = r.depth + 1;
|
|
101
|
+
return k;
|
|
102
|
+
});
|
|
103
|
+
const present = new Set(this.keys);
|
|
104
|
+
for (const k of [...this.collapsed])
|
|
105
|
+
if (!present.has(k))
|
|
106
|
+
this.collapsed.delete(k);
|
|
107
|
+
if (this.selKey && !present.has(this.selKey)) {
|
|
108
|
+
this.selKey = null;
|
|
109
|
+
this.selRowId = null;
|
|
110
|
+
this.details = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Rows minus descendants of folded parents. */
|
|
114
|
+
visibleRows() {
|
|
115
|
+
const out = [];
|
|
116
|
+
const skipDepths = [];
|
|
117
|
+
for (let i = 0; i < this.rows.length; i++) {
|
|
118
|
+
const r = this.rows[i];
|
|
119
|
+
const k = this.keys[i];
|
|
120
|
+
while (skipDepths.length && r.depth <= skipDepths[skipDepths.length - 1])
|
|
121
|
+
skipDepths.pop();
|
|
122
|
+
if (!skipDepths.length)
|
|
123
|
+
out.push({ row: r, key: k });
|
|
124
|
+
if (r.childCount > 0 && this.collapsed.has(k))
|
|
125
|
+
skipDepths.push(r.depth);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
localDetails(r) {
|
|
130
|
+
return [
|
|
131
|
+
`label: ${r.label}`,
|
|
132
|
+
r.detail ? `detail: ${r.detail}` : "",
|
|
133
|
+
`kind: ${r.kind === 1 ? "ecs entity" : "scene node"}`,
|
|
134
|
+
`children: ${r.childCount}`,
|
|
135
|
+
`depth: ${r.depth}`,
|
|
136
|
+
r.nodeId ? `nodeId: ${r.nodeId}` : "nodeId: (none — pick can't reach the inspector)",
|
|
137
|
+
].filter(Boolean).join("\n");
|
|
138
|
+
}
|
|
139
|
+
async loadDetails(row, key) {
|
|
140
|
+
this.details = { title: row.label, body: this.localDetails(row) };
|
|
141
|
+
this.shellDirty = true;
|
|
142
|
+
const nodeId = row.nodeId;
|
|
143
|
+
if (!nodeId)
|
|
144
|
+
return;
|
|
145
|
+
// Best-effort inspector enrichment + in-scene selection highlight.
|
|
146
|
+
try {
|
|
147
|
+
await this.ctx.call("inspector.call", { method: "selectNode", args: [nodeId] });
|
|
148
|
+
}
|
|
149
|
+
catch { /* inspector absent or id unknown — local details stand */ }
|
|
150
|
+
try {
|
|
151
|
+
const json = await this.ctx.call("inspector.call", { method: "getNodeJSON", args: [nodeId] });
|
|
152
|
+
// A newer pick while this call was in flight wins.
|
|
153
|
+
if (this.selKey === key && typeof json === "string" && json) {
|
|
154
|
+
this.details = { title: row.label, body: json };
|
|
155
|
+
this.shellDirty = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch { /* no inspector — keep the row-level details */ }
|
|
159
|
+
}
|
|
160
|
+
// ── Events ──
|
|
39
161
|
onBackendEvent(event, data) {
|
|
40
|
-
// "scene" pushes feed
|
|
162
|
+
// "scene" pushes feed scene mode; "dom" pushes feed ECS mode.
|
|
41
163
|
if (event === "scene" && this.mode === "scene") {
|
|
42
|
-
this.
|
|
43
|
-
this.dirty = true;
|
|
164
|
+
this.applyRows(data ?? []);
|
|
44
165
|
}
|
|
45
|
-
else if (event === "dom" && this.mode
|
|
46
|
-
this.
|
|
47
|
-
this.dirty = true;
|
|
166
|
+
else if (event === "dom" && this.mode === "ecs") {
|
|
167
|
+
this.applyRows(data ?? []);
|
|
48
168
|
}
|
|
49
169
|
}
|
|
50
170
|
onAction(data, _ev) {
|
|
51
|
-
|
|
171
|
+
const act = data.action;
|
|
172
|
+
if (act === "el.refresh") {
|
|
52
173
|
void this.refresh();
|
|
53
174
|
}
|
|
54
|
-
else if (
|
|
175
|
+
else if (act === "el.mode") {
|
|
55
176
|
const m = data.mode;
|
|
56
|
-
if (m && m !== this.mode) {
|
|
177
|
+
if ((m === "scene" || m === "ecs") && m !== this.mode) {
|
|
57
178
|
this.mode = m;
|
|
179
|
+
this.selKey = null;
|
|
180
|
+
this.selRowId = null;
|
|
181
|
+
this.details = null;
|
|
182
|
+
this.collapsed.clear();
|
|
58
183
|
this.shellDirty = true;
|
|
184
|
+
// Point the backend's periodic push at the mode being viewed.
|
|
185
|
+
void this.ctx.call("domTree.mode", { mode: m }).catch(() => { });
|
|
59
186
|
void this.refresh();
|
|
60
187
|
}
|
|
61
188
|
}
|
|
189
|
+
else if (act === "el.fold") {
|
|
190
|
+
const id = Number(data.node);
|
|
191
|
+
const i = this.rows.findIndex((r) => r.id === id);
|
|
192
|
+
if (i < 0)
|
|
193
|
+
return;
|
|
194
|
+
const key = this.keys[i];
|
|
195
|
+
if (this.collapsed.has(key))
|
|
196
|
+
this.collapsed.delete(key);
|
|
197
|
+
else
|
|
198
|
+
this.collapsed.add(key);
|
|
199
|
+
this.dirty = true;
|
|
200
|
+
this.shellDirty = true;
|
|
201
|
+
}
|
|
202
|
+
else if (act === "el.pick") {
|
|
203
|
+
this.pick(Number(data.node));
|
|
204
|
+
}
|
|
205
|
+
else if (act === "el.unpick") {
|
|
206
|
+
this.unpick();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
pick(id) {
|
|
210
|
+
const i = this.rows.findIndex((r) => r.id === id);
|
|
211
|
+
if (i < 0)
|
|
212
|
+
return;
|
|
213
|
+
const row = this.rows[i];
|
|
214
|
+
const key = this.keys[i];
|
|
215
|
+
this.selKey = key;
|
|
216
|
+
// Flip .sel via targeted mutations instead of a body re-render —
|
|
217
|
+
// reparse would reset the scrollport mid-browse. Falls back to a full
|
|
218
|
+
// render on backends without incremental ops.
|
|
219
|
+
const ops = [];
|
|
220
|
+
if (this.selRowId !== null && this.selRowId !== id) {
|
|
221
|
+
ops.push({ op: "attr", sel: `[data-node="${this.selRowId}"]`, name: "class", value: "trow" });
|
|
222
|
+
}
|
|
223
|
+
ops.push({ op: "attr", sel: `[data-node="${id}"]`, name: "class", value: "trow sel" });
|
|
224
|
+
this.ctx.mutate(ops);
|
|
225
|
+
this.selRowId = id;
|
|
226
|
+
void this.loadDetails(row, key);
|
|
227
|
+
}
|
|
228
|
+
unpick() {
|
|
229
|
+
if (this.selRowId !== null) {
|
|
230
|
+
this.ctx.mutate([{ op: "attr", sel: `[data-node="${this.selRowId}"]`, name: "class", value: "trow" }]);
|
|
231
|
+
}
|
|
232
|
+
this.selKey = null;
|
|
233
|
+
this.selRowId = null;
|
|
234
|
+
this.details = null;
|
|
235
|
+
this.shellDirty = true;
|
|
236
|
+
void this.ctx.call("inspector.call", { method: "selectNode", args: [null] }).catch(() => { });
|
|
237
|
+
}
|
|
238
|
+
onEvent(ev) {
|
|
239
|
+
if (ev.t === "scroll" && ev.id === "dt-body") {
|
|
240
|
+
this.lastScrollTop = ev.st ?? 0;
|
|
241
|
+
}
|
|
62
242
|
}
|
|
63
243
|
activate() {
|
|
244
|
+
// Keep the backend's push mode aligned with what's on screen.
|
|
245
|
+
void this.ctx.call("domTree.mode", { mode: this.mode }).catch(() => { });
|
|
64
246
|
void this.refresh();
|
|
65
247
|
}
|
|
248
|
+
afterRender() {
|
|
249
|
+
// Body reparse dropped the scroll position — put it back.
|
|
250
|
+
if (this.lastScrollTop > 0)
|
|
251
|
+
this.ctx.scrollTo("#dt-body", 0, this.lastScrollTop);
|
|
252
|
+
}
|
|
66
253
|
async refresh() {
|
|
67
254
|
try {
|
|
68
255
|
const rows = this.mode === "scene"
|
|
69
256
|
? await this.ctx.call("sceneTree")
|
|
70
|
-
: await this.ctx.call("domTree", { mode:
|
|
71
|
-
this.
|
|
257
|
+
: await this.ctx.call("domTree", { mode: "ecs" });
|
|
258
|
+
this.lastError = "";
|
|
259
|
+
this.applyRows(rows ?? []);
|
|
72
260
|
}
|
|
73
|
-
catch {
|
|
74
|
-
|
|
261
|
+
catch (err) {
|
|
262
|
+
// Keep the last good tree; surface the failure instead of blanking.
|
|
263
|
+
this.lastError = `refresh failed: ${String(err)}`;
|
|
75
264
|
}
|
|
76
265
|
this.dirty = true;
|
|
77
|
-
this.shellDirty = true;
|
|
266
|
+
this.shellDirty = true;
|
|
78
267
|
}
|
|
79
268
|
}
|
|
@@ -19,10 +19,13 @@ export class GpuPanel {
|
|
|
19
19
|
renderBody() {
|
|
20
20
|
if (!this.info)
|
|
21
21
|
return `<div class="empty-note">no GPU info</div>`;
|
|
22
|
-
const times = (this.info.frameTimes
|
|
22
|
+
const times = (Array.isArray(this.info.frameTimes) ? this.info.frameTimes : [])
|
|
23
|
+
.map((t) => (Array.isArray(t) ? t[0] : 0));
|
|
23
24
|
const chart = `<div class="snap-section"><h3>Frame times (ms)</h3>${chartHtml(times, { max: Math.max(16.7, ...times), label: "" })}</div>`;
|
|
24
25
|
const sections = [];
|
|
25
|
-
for (const e of this.info.entries
|
|
26
|
+
for (const e of (Array.isArray(this.info.entries) ? this.info.entries : []).values()) {
|
|
27
|
+
if (!e || typeof e !== "object")
|
|
28
|
+
continue;
|
|
26
29
|
if (e.isHeader || !sections.length) {
|
|
27
30
|
sections.push({ header: e.isHeader ? e.key : null, rows: [] });
|
|
28
31
|
if (e.isHeader)
|