@three-flatland/devtools 0.1.0-alpha.3 → 1.0.0-alpha.6
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 +1 -1
- package/dist/buffers-modal.d.ts +16 -52
- package/dist/buffers-modal.d.ts.map +1 -0
- package/dist/buffers-modal.js +459 -464
- package/dist/buffers-modal.js.map +1 -1
- package/dist/buffers-view.d.ts +22 -34
- package/dist/buffers-view.d.ts.map +1 -0
- package/dist/buffers-view.js +252 -240
- package/dist/buffers-view.js.map +1 -1
- package/dist/create-pane.d.ts +38 -37
- package/dist/create-pane.d.ts.map +1 -0
- package/dist/create-pane.js +197 -166
- package/dist/create-pane.js.map +1 -1
- package/dist/dashboard/app.tsx +2 -0
- package/dist/dashboard/export.ts +1 -3
- package/dist/dashboard/frame-cursor.ts +81 -0
- package/dist/dashboard/hooks.ts +10 -2
- package/dist/dashboard/index.html +61 -0
- package/dist/dashboard/panels/batches.tsx +5 -18
- package/dist/dashboard/panels/buffers.tsx +52 -26
- package/dist/dashboard/panels/env.tsx +5 -6
- package/dist/dashboard/panels/protocol-log.tsx +146 -58
- package/dist/dashboard/panels/registry.tsx +55 -69
- package/dist/dashboard/panels/scrubber.tsx +100 -0
- package/dist/dashboard/panels/stats.tsx +60 -32
- package/dist/dashboard/protocol-store.ts +25 -8
- package/dist/dashboard/vendor/hooks.module.d.ts +146 -0
- package/dist/dashboard/vendor/jsx-runtime.d.ts +55 -0
- package/dist/dashboard/vendor/jsx.d.ts +3123 -0
- package/dist/dashboard/vendor/preact.module.d.ts +397 -0
- package/dist/devtools-client.d.ts +312 -300
- package/dist/devtools-client.d.ts.map +1 -0
- package/dist/devtools-client.js +866 -838
- package/dist/devtools-client.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.js +5 -12
- package/dist/perf-trace.d.ts +6 -4
- package/dist/perf-trace.d.ts.map +1 -0
- package/dist/perf-trace.js +80 -66
- package/dist/perf-trace.js.map +1 -1
- package/dist/plugins.d.ts +6 -6
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +5 -5
- package/dist/plugins.js.map +1 -1
- package/dist/provider-switcher.d.ts +8 -15
- package/dist/provider-switcher.d.ts.map +1 -0
- package/dist/provider-switcher.js +68 -68
- package/dist/provider-switcher.js.map +1 -1
- package/dist/react/devtools-provider.d.ts +11 -10
- package/dist/react/devtools-provider.d.ts.map +1 -0
- package/dist/react/devtools-provider.js +60 -32
- package/dist/react/devtools-provider.js.map +1 -1
- package/dist/react/use-fps-graph.d.ts +7 -7
- package/dist/react/use-fps-graph.d.ts.map +1 -0
- package/dist/react/use-fps-graph.js +27 -21
- package/dist/react/use-fps-graph.js.map +1 -1
- package/dist/react/use-pane-button.d.ts +4 -4
- package/dist/react/use-pane-button.d.ts.map +1 -0
- package/dist/react/use-pane-button.js +35 -30
- package/dist/react/use-pane-button.js.map +1 -1
- package/dist/react/use-pane-folder.d.ts +5 -4
- package/dist/react/use-pane-folder.d.ts.map +1 -0
- package/dist/react/use-pane-folder.js +33 -20
- package/dist/react/use-pane-folder.js.map +1 -1
- package/dist/react/use-pane-input.d.ts +31 -30
- package/dist/react/use-pane-input.d.ts.map +1 -0
- package/dist/react/use-pane-input.js +46 -42
- package/dist/react/use-pane-input.js.map +1 -1
- package/dist/react/use-pane-radio-grid.d.ts +20 -20
- package/dist/react/use-pane-radio-grid.d.ts.map +1 -0
- package/dist/react/use-pane-radio-grid.js +72 -50
- package/dist/react/use-pane-radio-grid.js.map +1 -1
- package/dist/react/use-pane.d.ts +4 -4
- package/dist/react/use-pane.d.ts.map +1 -0
- package/dist/react/use-pane.js +52 -17
- package/dist/react/use-pane.js.map +1 -1
- package/dist/react.d.ts +10 -12
- package/dist/react.js +2 -12
- package/dist/registry-view.d.ts +8 -21
- package/dist/registry-view.d.ts.map +1 -0
- package/dist/registry-view.js +357 -338
- package/dist/registry-view.js.map +1 -1
- package/dist/relay.d.ts +16 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +189 -0
- package/dist/relay.js.map +1 -0
- package/dist/remote.d.ts +10 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +28 -0
- package/dist/remote.js.map +1 -0
- package/dist/stats-graph.d.ts +23 -31
- package/dist/stats-graph.d.ts.map +1 -0
- package/dist/stats-graph.js +323 -264
- package/dist/stats-graph.js.map +1 -1
- package/dist/stats-row.d.ts +8 -21
- package/dist/stats-row.d.ts.map +1 -0
- package/dist/stats-row.js +99 -101
- package/dist/stats-row.js.map +1 -1
- package/dist/theme.d.ts +3 -1
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +43 -54
- package/dist/theme.js.map +1 -1
- package/dist/vite-plugin.d.ts +7 -6
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +87 -78
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +33 -29
- package/dist/buffers-modal.cjs +0 -491
- package/dist/buffers-modal.cjs.map +0 -1
- package/dist/buffers-modal.d.cts +0 -62
- package/dist/buffers-view.cjs +0 -269
- package/dist/buffers-view.cjs.map +0 -1
- package/dist/buffers-view.d.cts +0 -39
- package/dist/create-pane.cjs +0 -203
- package/dist/create-pane.cjs.map +0 -1
- package/dist/create-pane.d.cts +0 -66
- package/dist/devtools-client.cjs +0 -870
- package/dist/devtools-client.cjs.map +0 -1
- package/dist/devtools-client.d.cts +0 -362
- package/dist/index.cjs +0 -42
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -8
- package/dist/index.js.map +0 -1
- package/dist/perf-trace.cjs +0 -103
- package/dist/perf-trace.cjs.map +0 -1
- package/dist/perf-trace.d.cts +0 -27
- package/dist/plugins.cjs +0 -44
- package/dist/plugins.cjs.map +0 -1
- package/dist/plugins.d.cts +0 -7
- package/dist/provider-switcher.cjs +0 -95
- package/dist/provider-switcher.cjs.map +0 -1
- package/dist/provider-switcher.d.cts +0 -18
- package/dist/react/devtools-provider.cjs +0 -59
- package/dist/react/devtools-provider.cjs.map +0 -1
- package/dist/react/devtools-provider.d.cts +0 -39
- package/dist/react/use-fps-graph.cjs +0 -49
- package/dist/react/use-fps-graph.cjs.map +0 -1
- package/dist/react/use-fps-graph.d.cts +0 -15
- package/dist/react/use-pane-button.cjs +0 -58
- package/dist/react/use-pane-button.cjs.map +0 -1
- package/dist/react/use-pane-button.d.cts +0 -10
- package/dist/react/use-pane-folder.cjs +0 -48
- package/dist/react/use-pane-folder.cjs.map +0 -1
- package/dist/react/use-pane-folder.d.cts +0 -15
- package/dist/react/use-pane-input.cjs +0 -70
- package/dist/react/use-pane-input.cjs.map +0 -1
- package/dist/react/use-pane-input.d.cts +0 -44
- package/dist/react/use-pane-radio-grid.cjs +0 -78
- package/dist/react/use-pane-radio-grid.cjs.map +0 -1
- package/dist/react/use-pane-radio-grid.d.cts +0 -35
- package/dist/react/use-pane.cjs +0 -45
- package/dist/react/use-pane.cjs.map +0 -1
- package/dist/react/use-pane.d.cts +0 -40
- package/dist/react.cjs +0 -50
- package/dist/react.cjs.map +0 -1
- package/dist/react.d.cts +0 -12
- package/dist/react.js.map +0 -1
- package/dist/registry-view.cjs +0 -371
- package/dist/registry-view.cjs.map +0 -1
- package/dist/registry-view.d.cts +0 -24
- package/dist/stats-graph.cjs +0 -297
- package/dist/stats-graph.cjs.map +0 -1
- package/dist/stats-graph.d.cts +0 -40
- package/dist/stats-row.cjs +0 -153
- package/dist/stats-row.cjs.map +0 -1
- package/dist/stats-row.d.cts +0 -24
- package/dist/theme.cjs +0 -260
- package/dist/theme.cjs.map +0 -1
- package/dist/theme.d.cts +0 -10
- package/dist/vite-plugin.cjs +0 -113
- package/dist/vite-plugin.cjs.map +0 -1
- package/dist/vite-plugin.d.cts +0 -9
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-travel frame cursor (#29 Phase A — passive frame-link).
|
|
3
|
+
*
|
|
4
|
+
* `null` = live (panels tail as usual). A number parks every panel at
|
|
5
|
+
* that engine frame: stats read the ring sample at frame ≤ cursor, the
|
|
6
|
+
* protocol log scrolls to the nearest row and stops tailing, buffers
|
|
7
|
+
* freeze (historical playback is the Phase C flight recorder).
|
|
8
|
+
*
|
|
9
|
+
* Cursors are remembered per provider — switching producers restores
|
|
10
|
+
* that producer's parked position (or live).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type Listener = () => void
|
|
14
|
+
|
|
15
|
+
const cursorByProvider = new Map<string, number>()
|
|
16
|
+
let activeProviderId: string | null = null
|
|
17
|
+
let liveCursor: number | null = null
|
|
18
|
+
const listeners = new Set<Listener>()
|
|
19
|
+
|
|
20
|
+
function fire(): void {
|
|
21
|
+
for (const cb of listeners) {
|
|
22
|
+
try {
|
|
23
|
+
cb()
|
|
24
|
+
} catch {
|
|
25
|
+
/* listener errors shouldn't break the cursor */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function addFrameCursorListener(cb: Listener): () => void {
|
|
31
|
+
listeners.add(cb)
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(cb)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The parked frame, or null when live. */
|
|
38
|
+
export function getFrameCursor(): number | null {
|
|
39
|
+
return liveCursor
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Park at a frame (clamped by callers to their known range). Passing
|
|
44
|
+
* `null` deletes the active provider's parked cursor entirely, not
|
|
45
|
+
* just sets it to "live" — the next `setCursorProvider` switch away
|
|
46
|
+
* and back finds no stored entry and comes up live rather than
|
|
47
|
+
* re-parking at the old frame.
|
|
48
|
+
*/
|
|
49
|
+
export function setFrameCursor(frame: number | null): void {
|
|
50
|
+
if (liveCursor === frame) return
|
|
51
|
+
liveCursor = frame
|
|
52
|
+
if (activeProviderId !== null) {
|
|
53
|
+
if (frame === null) cursorByProvider.delete(activeProviderId)
|
|
54
|
+
else cursorByProvider.set(activeProviderId, frame)
|
|
55
|
+
}
|
|
56
|
+
fire()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Return to live. Deletes the active provider's parked cursor (see
|
|
61
|
+
* `setFrameCursor`) — intentional: a manual go-live is a decision to
|
|
62
|
+
* stop watching a specific frame, so switching providers and back
|
|
63
|
+
* should not silently re-park.
|
|
64
|
+
*/
|
|
65
|
+
export function goLive(): void {
|
|
66
|
+
setFrameCursor(null)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Provider switch: remember the outgoing provider's cursor, restore
|
|
71
|
+
* the incoming one's (per-provider cursor semantics).
|
|
72
|
+
*/
|
|
73
|
+
export function setCursorProvider(providerId: string | null): void {
|
|
74
|
+
if (activeProviderId === providerId) return
|
|
75
|
+
activeProviderId = providerId
|
|
76
|
+
const restored = providerId !== null ? (cursorByProvider.get(providerId) ?? null) : null
|
|
77
|
+
if (liveCursor !== restored) {
|
|
78
|
+
liveCursor = restored
|
|
79
|
+
fire()
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/dashboard/hooks.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { useEffect, useState } from 'preact/hooks'
|
|
|
10
10
|
import type { DevtoolsState } from '../devtools-client.js'
|
|
11
11
|
import { getClient } from './client.js'
|
|
12
12
|
import { getProtocolStore } from './protocol-store.js'
|
|
13
|
+
import { addFrameCursorListener } from './frame-cursor.js'
|
|
13
14
|
|
|
14
15
|
type Subscriber = () => void
|
|
15
16
|
|
|
@@ -25,14 +26,20 @@ function scheduleFrame(): void {
|
|
|
25
26
|
requestAnimationFrame(() => {
|
|
26
27
|
_frameScheduled = false
|
|
27
28
|
for (const cb of _frameListeners) {
|
|
28
|
-
try {
|
|
29
|
+
try {
|
|
30
|
+
cb()
|
|
31
|
+
} catch {
|
|
32
|
+
/* listener errors shouldn't break the bus */
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
})
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
function subscribeFrame(cb: Subscriber): () => void {
|
|
34
39
|
_frameListeners.add(cb)
|
|
35
|
-
return () => {
|
|
40
|
+
return () => {
|
|
41
|
+
_frameListeners.delete(cb)
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
// Wire the underlying data sources to the shared rAF once. Any event on
|
|
@@ -43,6 +50,7 @@ function ensureSourcesWired(): void {
|
|
|
43
50
|
_sourcesWired = true
|
|
44
51
|
getClient().addListener(scheduleFrame)
|
|
45
52
|
getProtocolStore().addListener(scheduleFrame)
|
|
53
|
+
addFrameCursorListener(scheduleFrame)
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
/**
|
|
@@ -1140,6 +1140,67 @@
|
|
|
1140
1140
|
.registry-panel { flex: initial; height: 300px; }
|
|
1141
1141
|
.protocol-panel { flex: initial; height: 420px; }
|
|
1142
1142
|
}
|
|
1143
|
+
|
|
1144
|
+
/* ── Time-travel scrubber (#29 Phase A) ─────────────────────────── */
|
|
1145
|
+
.frame-scrubber {
|
|
1146
|
+
display: flex;
|
|
1147
|
+
align-items: center;
|
|
1148
|
+
gap: 8px;
|
|
1149
|
+
padding: 4px 10px;
|
|
1150
|
+
border-bottom: 1px solid var(--border, #22262e);
|
|
1151
|
+
font-size: 11px;
|
|
1152
|
+
color: var(--fg-muted, #8b93a3);
|
|
1153
|
+
}
|
|
1154
|
+
.frame-scrubber.parked {
|
|
1155
|
+
background: color-mix(in srgb, #d94c87 8%, transparent);
|
|
1156
|
+
}
|
|
1157
|
+
.frame-scrubber .scrub-slider {
|
|
1158
|
+
flex: 1;
|
|
1159
|
+
min-width: 80px;
|
|
1160
|
+
accent-color: #d94c87;
|
|
1161
|
+
}
|
|
1162
|
+
.frame-scrubber .scrub-step,
|
|
1163
|
+
.frame-scrubber .scrub-live {
|
|
1164
|
+
background: none;
|
|
1165
|
+
border: 1px solid var(--border, #22262e);
|
|
1166
|
+
border-radius: 3px;
|
|
1167
|
+
color: inherit;
|
|
1168
|
+
cursor: pointer;
|
|
1169
|
+
font: inherit;
|
|
1170
|
+
padding: 1px 6px;
|
|
1171
|
+
}
|
|
1172
|
+
.frame-scrubber .scrub-live.active {
|
|
1173
|
+
color: #47cca9;
|
|
1174
|
+
border-color: #47cca9;
|
|
1175
|
+
}
|
|
1176
|
+
.frame-scrubber.parked .scrub-live {
|
|
1177
|
+
color: #d94c87;
|
|
1178
|
+
border-color: #d94c87;
|
|
1179
|
+
}
|
|
1180
|
+
.frame-scrubber .scrub-frame {
|
|
1181
|
+
font-variant-numeric: tabular-nums;
|
|
1182
|
+
min-width: 9ch;
|
|
1183
|
+
text-align: right;
|
|
1184
|
+
}
|
|
1185
|
+
.stat-value.stat-parked {
|
|
1186
|
+
color: #d94c87;
|
|
1187
|
+
}
|
|
1188
|
+
.buffers-parked-note,
|
|
1189
|
+
.registry-parked-note {
|
|
1190
|
+
position: absolute;
|
|
1191
|
+
inset-inline: 0;
|
|
1192
|
+
bottom: 0;
|
|
1193
|
+
padding: 4px 8px;
|
|
1194
|
+
font-size: 11px;
|
|
1195
|
+
color: #d94c87;
|
|
1196
|
+
background: color-mix(in srgb, #0d1016 82%, transparent);
|
|
1197
|
+
pointer-events: none;
|
|
1198
|
+
z-index: 2;
|
|
1199
|
+
}
|
|
1200
|
+
.registry-parked-note {
|
|
1201
|
+
position: static;
|
|
1202
|
+
pointer-events: auto;
|
|
1203
|
+
}
|
|
1143
1204
|
</style>
|
|
1144
1205
|
</head>
|
|
1145
1206
|
<body>
|
|
@@ -95,9 +95,7 @@ export function BatchesPanel() {
|
|
|
95
95
|
run.batches.push({ batchIdx: b.batchIdx, spriteCount: b.spriteCount, label: b.label })
|
|
96
96
|
}
|
|
97
97
|
const arr = Array.from(m.values())
|
|
98
|
-
arr.sort((a, b) =>
|
|
99
|
-
a.kind.localeCompare(b.kind) || a.layer - b.layer || a.materialId - b.materialId,
|
|
100
|
-
)
|
|
98
|
+
arr.sort((a, b) => a.kind.localeCompare(b.kind) || a.layer - b.layer || a.materialId - b.materialId)
|
|
101
99
|
return arr
|
|
102
100
|
}, [batches.frame])
|
|
103
101
|
|
|
@@ -131,7 +129,8 @@ export function BatchesPanel() {
|
|
|
131
129
|
<header class="panel-header batches-header">
|
|
132
130
|
<span>Batches</span>
|
|
133
131
|
<span class="batches-header-meta">
|
|
134
|
-
frame {batches.frame} · {passTotals.calls} draws · {runs.length} runs · {batches.batches.length} batches ·
|
|
132
|
+
frame {batches.frame} · {passTotals.calls} draws · {runs.length} runs · {batches.batches.length} batches ·{' '}
|
|
133
|
+
{totalSprites} sprites
|
|
135
134
|
</span>
|
|
136
135
|
</header>
|
|
137
136
|
|
|
@@ -196,12 +195,7 @@ export function BatchesPanel() {
|
|
|
196
195
|
<li class="batches-empty">no passes captured</li>
|
|
197
196
|
) : (
|
|
198
197
|
passRoots.map((root) => (
|
|
199
|
-
<PassRow
|
|
200
|
-
key={root.index}
|
|
201
|
-
node={root}
|
|
202
|
-
collapsed={collapsedPasses}
|
|
203
|
-
onToggle={toggleCollapsed}
|
|
204
|
-
/>
|
|
198
|
+
<PassRow key={root.index} node={root} collapsed={collapsedPasses} onToggle={toggleCollapsed} />
|
|
205
199
|
))
|
|
206
200
|
)}
|
|
207
201
|
</ul>
|
|
@@ -263,14 +257,7 @@ function PassRow({
|
|
|
263
257
|
<span class="batches-col-num">{pass.cpuMs.toFixed(2)}</span>
|
|
264
258
|
</li>
|
|
265
259
|
{hasChildren && !isCollapsed
|
|
266
|
-
? children.map((c) =>
|
|
267
|
-
<PassRow
|
|
268
|
-
key={c.index}
|
|
269
|
-
node={c}
|
|
270
|
-
collapsed={collapsed}
|
|
271
|
-
onToggle={onToggle}
|
|
272
|
-
/>
|
|
273
|
-
))
|
|
260
|
+
? children.map((c) => <PassRow key={c.index} node={c} collapsed={collapsed} onToggle={onToggle} />)
|
|
274
261
|
: null}
|
|
275
262
|
</>
|
|
276
263
|
)
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
|
19
19
|
import type { BufferChunkPayload } from '../../devtools-client.js'
|
|
20
20
|
import { getClient } from '../client.js'
|
|
21
|
+
import { getFrameCursor } from '../frame-cursor.js'
|
|
21
22
|
import { useDevtoolsState } from '../hooks.js'
|
|
22
23
|
|
|
23
24
|
const VP9_CODEC = 'vp09.00.10.08'
|
|
@@ -63,18 +64,19 @@ export function BuffersPanel() {
|
|
|
63
64
|
}, [state.buffers, state.buffers.size])
|
|
64
65
|
|
|
65
66
|
const needle = filter.trim().toLowerCase()
|
|
66
|
-
const visible =
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const visible =
|
|
68
|
+
needle.length > 0
|
|
69
|
+
? entries.filter(
|
|
70
|
+
(e) => e.name.toLowerCase().includes(needle) || (e.label?.toLowerCase().includes(needle) ?? false)
|
|
71
|
+
)
|
|
72
|
+
: entries
|
|
69
73
|
|
|
70
74
|
// Only clear the selection if the currently-selected name has
|
|
71
75
|
// vanished from the registry. Never auto-pick — selection must be
|
|
72
76
|
// explicit so we don't silently subscribe a producer in stream mode
|
|
73
77
|
// the moment a buffer shows up. Unselected = nothing streaming, no
|
|
74
78
|
// GPU readback or VP9 encode on the producer side.
|
|
75
|
-
const effectiveSelected = selectedName !== null && visible.some((e) => e.name === selectedName)
|
|
76
|
-
? selectedName
|
|
77
|
-
: null
|
|
79
|
+
const effectiveSelected = selectedName !== null && visible.some((e) => e.name === selectedName) ? selectedName : null
|
|
78
80
|
|
|
79
81
|
// Subscribe/unsubscribe to stream mode based on selection. The dashboard
|
|
80
82
|
// is a fresh DevtoolsClient consumer so this subscription is additive
|
|
@@ -86,7 +88,9 @@ export function BuffersPanel() {
|
|
|
86
88
|
return
|
|
87
89
|
}
|
|
88
90
|
client.setBuffers({ [effectiveSelected]: { mode: 'stream' } })
|
|
89
|
-
return () => {
|
|
91
|
+
return () => {
|
|
92
|
+
client.setBuffers({})
|
|
93
|
+
}
|
|
90
94
|
}, [client, effectiveSelected])
|
|
91
95
|
|
|
92
96
|
// WebCodecs decoder lifecycle — one instance per active selection.
|
|
@@ -122,7 +126,11 @@ export function BuffersPanel() {
|
|
|
122
126
|
const stopDecoder = (): void => {
|
|
123
127
|
const d = decoderRef.current
|
|
124
128
|
if (d !== null && d.state !== 'closed') {
|
|
125
|
-
try {
|
|
129
|
+
try {
|
|
130
|
+
d.close()
|
|
131
|
+
} catch {
|
|
132
|
+
/* may already be errored */
|
|
133
|
+
}
|
|
126
134
|
}
|
|
127
135
|
decoderRef.current = null
|
|
128
136
|
decoderDimsRef.current = { w: 0, h: 0 }
|
|
@@ -139,6 +147,12 @@ export function BuffersPanel() {
|
|
|
139
147
|
waitingForKeyFrameRef.current = true
|
|
140
148
|
const decoder = new globalThis.VideoDecoder({
|
|
141
149
|
output: (frame) => {
|
|
150
|
+
// Parked cursor freezes the canvas (decode continues so the
|
|
151
|
+
// delta chain stays valid; we just don't paint).
|
|
152
|
+
if (getFrameCursor() !== null) {
|
|
153
|
+
frame.close()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
142
156
|
if (canvas.width !== frame.codedWidth || canvas.height !== frame.codedHeight) {
|
|
143
157
|
canvas.width = frame.codedWidth
|
|
144
158
|
canvas.height = frame.codedHeight
|
|
@@ -201,6 +215,9 @@ export function BuffersPanel() {
|
|
|
201
215
|
// Paint those when the stream path isn't decoding.
|
|
202
216
|
useEffect(() => {
|
|
203
217
|
if (CODEC_AVAILABLE) return
|
|
218
|
+
// Parked cursor freezes the canvas — the last-drawn pixels stay put
|
|
219
|
+
// until the user returns to live (historical playback is Phase C).
|
|
220
|
+
if (getFrameCursor() !== null) return
|
|
204
221
|
if (effectiveSelected === null) return
|
|
205
222
|
const snap = state.buffers.get(effectiveSelected)
|
|
206
223
|
if (snap === undefined || snap.pixels === null) return
|
|
@@ -304,7 +321,10 @@ export function BuffersPanel() {
|
|
|
304
321
|
const rect = canvas.getBoundingClientRect()
|
|
305
322
|
const u = (e.clientX - rect.left) / rect.width
|
|
306
323
|
const v = (e.clientY - rect.top) / rect.height
|
|
307
|
-
if (u < 0 || u > 1 || v < 0 || v > 1) {
|
|
324
|
+
if (u < 0 || u > 1 || v < 0 || v > 1) {
|
|
325
|
+
setProbe(null)
|
|
326
|
+
return
|
|
327
|
+
}
|
|
308
328
|
const px = Math.min(canvas.width - 1, Math.max(0, Math.floor(u * canvas.width)))
|
|
309
329
|
const py = Math.min(canvas.height - 1, Math.max(0, Math.floor(v * canvas.height)))
|
|
310
330
|
try {
|
|
@@ -323,7 +343,7 @@ export function BuffersPanel() {
|
|
|
323
343
|
setPan({ x: 0, y: 0 })
|
|
324
344
|
}
|
|
325
345
|
|
|
326
|
-
const selectedSnap = effectiveSelected !== null ? state.buffers.get(effectiveSelected) ?? null : null
|
|
346
|
+
const selectedSnap = effectiveSelected !== null ? (state.buffers.get(effectiveSelected) ?? null) : null
|
|
327
347
|
|
|
328
348
|
return (
|
|
329
349
|
<section class="panel buffers-panel">
|
|
@@ -347,17 +367,13 @@ export function BuffersPanel() {
|
|
|
347
367
|
<li key={e.name}>
|
|
348
368
|
<button
|
|
349
369
|
type="button"
|
|
350
|
-
class={
|
|
351
|
-
'buffers-row' +
|
|
352
|
-
(e.name === effectiveSelected ? ' buffers-row-selected' : '')
|
|
353
|
-
}
|
|
370
|
+
class={'buffers-row' + (e.name === effectiveSelected ? ' buffers-row-selected' : '')}
|
|
354
371
|
onClick={() => setSelectedName((prev) => (prev === e.name ? null : e.name))}
|
|
355
372
|
title={e.name === effectiveSelected ? 'Click again to stop streaming' : 'Stream this buffer'}
|
|
356
373
|
>
|
|
357
374
|
<span class="buffers-name">{e.name}</span>
|
|
358
375
|
<span class="buffers-meta">
|
|
359
|
-
{
|
|
360
|
-
{(e.srcHeight > 0 ? e.srcHeight : e.height)} · {e.pixelType}
|
|
376
|
+
{e.srcWidth > 0 ? e.srcWidth : e.width}×{e.srcHeight > 0 ? e.srcHeight : e.height} · {e.pixelType}
|
|
361
377
|
</span>
|
|
362
378
|
</button>
|
|
363
379
|
</li>
|
|
@@ -383,6 +399,11 @@ export function BuffersPanel() {
|
|
|
383
399
|
onMouseMove={onCanvasMove}
|
|
384
400
|
onMouseLeave={onCanvasLeave}
|
|
385
401
|
/>
|
|
402
|
+
{getFrameCursor() !== null ? (
|
|
403
|
+
<div class="buffers-parked-note">
|
|
404
|
+
parked at frame {getFrameCursor()} — no playback yet (flight recorder lands in Phase C); canvas frozen
|
|
405
|
+
</div>
|
|
406
|
+
) : null}
|
|
386
407
|
<div class="buffers-info">
|
|
387
408
|
<span>{selectedSnap.name}</span>
|
|
388
409
|
<span>
|
|
@@ -392,22 +413,25 @@ export function BuffersPanel() {
|
|
|
392
413
|
<span>{selectedSnap.pixelType}</span>
|
|
393
414
|
<span>{selectedSnap.display}</span>
|
|
394
415
|
{!CODEC_AVAILABLE && <span class="buffers-warn">no WebCodecs — thumbnail fallback</span>}
|
|
395
|
-
<span class="buffers-zoom">
|
|
416
|
+
<span class="buffers-zoom">
|
|
417
|
+
{zoom === 1 ? '1.0×' : `${zoom < 10 ? zoom.toFixed(1) : Math.round(zoom)}×`}
|
|
418
|
+
</span>
|
|
396
419
|
</div>
|
|
397
420
|
{probe !== null && (
|
|
398
421
|
<div class="buffers-probe">
|
|
399
|
-
<span>
|
|
422
|
+
<span>
|
|
423
|
+
({probe.x}, {probe.y})
|
|
424
|
+
</span>
|
|
400
425
|
<span class="probe-chip" style={{ background: `rgb(${probe.r}, ${probe.g}, ${probe.b})` }} />
|
|
401
|
-
<span>
|
|
426
|
+
<span>
|
|
427
|
+
{probe.r} {probe.g} {probe.b} {probe.a}
|
|
428
|
+
</span>
|
|
402
429
|
</div>
|
|
403
430
|
)}
|
|
404
431
|
{(zoom !== 1 || pan.x !== 0 || pan.y !== 0) && (
|
|
405
|
-
<button
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
onClick={resetView}
|
|
409
|
-
title="Reset zoom & pan"
|
|
410
|
-
>⟲</button>
|
|
432
|
+
<button type="button" class="buffers-reset" onClick={resetView} title="Reset zoom & pan">
|
|
433
|
+
⟲
|
|
434
|
+
</button>
|
|
411
435
|
)}
|
|
412
436
|
<button
|
|
413
437
|
type="button"
|
|
@@ -415,7 +439,9 @@ export function BuffersPanel() {
|
|
|
415
439
|
onClick={() => setSelectedName(null)}
|
|
416
440
|
title="Stop streaming"
|
|
417
441
|
aria-label="Stop streaming"
|
|
418
|
-
|
|
442
|
+
>
|
|
443
|
+
×
|
|
444
|
+
</button>
|
|
419
445
|
</>
|
|
420
446
|
)}
|
|
421
447
|
</div>
|
|
@@ -39,13 +39,12 @@ export function EnvPopover() {
|
|
|
39
39
|
}
|
|
40
40
|
}, [open])
|
|
41
41
|
|
|
42
|
-
const canvas =
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const canvas =
|
|
43
|
+
s.canvasWidth !== undefined && s.canvasHeight !== undefined
|
|
44
|
+
? `${s.canvasWidth}×${s.canvasHeight}${s.canvasPixelRatio !== undefined ? ` @${s.canvasPixelRatio}x` : ''}`
|
|
45
|
+
: '—'
|
|
45
46
|
|
|
46
|
-
const summary = s.backendName !== undefined
|
|
47
|
-
? s.backendName
|
|
48
|
-
: 'Env'
|
|
47
|
+
const summary = s.backendName !== undefined ? s.backendName : 'Env'
|
|
49
48
|
|
|
50
49
|
return (
|
|
51
50
|
<div class="env-popover" ref={rootRef}>
|