@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
package/dist/create-pane.d.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { Pane } from
|
|
2
|
-
|
|
1
|
+
import { Pane } from "tweakpane";
|
|
2
|
+
//#region src/create-pane.d.ts
|
|
3
3
|
interface CreatePaneOptions {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
4
|
+
/** Custom container element */
|
|
5
|
+
container?: HTMLElement;
|
|
6
|
+
/** Pane title (default: 'Controls') */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** Default expansion state (default: true) */
|
|
9
|
+
expanded?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Who drives the stats-graph render?
|
|
12
|
+
* - `'raf'` (default): the pane starts its own `requestAnimationFrame`
|
|
13
|
+
* loop. Zero setup; fine for simple apps.
|
|
14
|
+
* - `'manual'`: no internal rAF. Call `bundle.update()` from your own
|
|
15
|
+
* frame tick — `renderer.setAnimationLoop` callback, R3F `useFrame`,
|
|
16
|
+
* or any ticker you already own. Preferred when you have a render
|
|
17
|
+
* loop; avoids a second rAF (which Safari throttles).
|
|
18
|
+
*/
|
|
19
|
+
driver?: 'raf' | 'manual';
|
|
20
20
|
}
|
|
21
21
|
interface PaneBundle {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
pane: Pane;
|
|
23
|
+
/**
|
|
24
|
+
* Render a stats-graph frame. Call from your existing animation loop
|
|
25
|
+
* (R3F `useFrame`, `renderer.setAnimationLoop`, etc.) for shared-tick
|
|
26
|
+
* visuals — avoids a second `requestAnimationFrame` callback, which
|
|
27
|
+
* Safari penalises.
|
|
28
|
+
*
|
|
29
|
+
* If you never call `update()`, the graph falls back to its own rAF
|
|
30
|
+
* so the pane works out-of-the-box in apps without a render loop.
|
|
31
|
+
*/
|
|
32
|
+
update(): void;
|
|
33
|
+
/**
|
|
34
|
+
* `true` once `pane.dispose()` has been called. Lets React hooks
|
|
35
|
+
* detect that a prior cleanup tore the bundle down and decide
|
|
36
|
+
* whether to recreate (StrictMode mount→cleanup→mount).
|
|
37
|
+
*/
|
|
38
|
+
readonly disposed: boolean;
|
|
39
39
|
}
|
|
40
40
|
declare function claimPane(bundle: PaneBundle): void;
|
|
41
41
|
/**
|
|
@@ -62,5 +62,6 @@ declare function claimPane(bundle: PaneBundle): void;
|
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
64
|
declare function createPane(options?: CreatePaneOptions): PaneBundle;
|
|
65
|
-
|
|
66
|
-
export {
|
|
65
|
+
//#endregion
|
|
66
|
+
export { CreatePaneOptions, PaneBundle, claimPane, createPane };
|
|
67
|
+
//# sourceMappingURL=create-pane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-pane.d.ts","names":[],"sources":["../src/create-pane.ts"],"mappings":";;UAWiB;;EAEf,YAAY;;EAEZ;;EAEA;;;;;;;;;;EAUA;;UAGe;EACf,MAAM;;;;;;;;;;EAUN;;;;;;WAMS;;iBAqBK,UAAU,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;iBA2BlB,WAAW,UAAS,oBAAyB"}
|
package/dist/create-pane.js
CHANGED
|
@@ -1,178 +1,209 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createBuffersModal } from "./buffers-modal.js";
|
|
2
|
+
import { addBuffersView } from "./buffers-view.js";
|
|
3
|
+
import { FLATLAND_THEME, applyTheme } from "./theme.js";
|
|
3
4
|
import { registerPlugins } from "./plugins.js";
|
|
4
5
|
import { addStatsGraph } from "./stats-graph.js";
|
|
5
6
|
import { addStatsRow } from "./stats-row.js";
|
|
6
7
|
import { addProviderSwitcher } from "./provider-switcher.js";
|
|
7
8
|
import { addRegistryView } from "./registry-view.js";
|
|
8
|
-
import { addBuffersView } from "./buffers-view.js";
|
|
9
|
-
import { createBuffersModal } from "./buffers-modal.js";
|
|
10
9
|
import { DevtoolsClient } from "./devtools-client.js";
|
|
10
|
+
import { Pane } from "tweakpane";
|
|
11
|
+
//#region src/create-pane.ts
|
|
12
|
+
/**
|
|
13
|
+
* Module-level "unclaimed pane" slot.
|
|
14
|
+
*
|
|
15
|
+
* React 18+ with StrictMode and Suspense can render a component multiple
|
|
16
|
+
* times before the first effect commits (each aborted render discards its
|
|
17
|
+
* `useRef` slots, including `bundleRef`). Because `new Pane()` mounts a
|
|
18
|
+
* wrapper element to `document.body` synchronously, every aborted attempt
|
|
19
|
+
* would leak a pane into the DOM (each with its own RAF loop).
|
|
20
|
+
*
|
|
21
|
+
* This slot tracks the most recent pane that hasn't been "claimed" by a
|
|
22
|
+
* committed `useEffect`. When `createPane` is called again, any still-
|
|
23
|
+
* unclaimed pane is disposed first — cleaning up orphans from the previous
|
|
24
|
+
* discarded render. Once `usePane`'s effect commits, it calls `claimPane`
|
|
25
|
+
* to prevent the current pane from being disposed by future `createPane`
|
|
26
|
+
* calls in unrelated components.
|
|
27
|
+
*/
|
|
11
28
|
let _unclaimedPane = null;
|
|
12
29
|
function claimPane(bundle) {
|
|
13
|
-
|
|
30
|
+
if (_unclaimedPane === bundle) _unclaimedPane = null;
|
|
14
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a themed Tweakpane instance with collapsible header and stats.
|
|
34
|
+
*
|
|
35
|
+
* Layout:
|
|
36
|
+
* [Controls ▾] ← collapsible pane header
|
|
37
|
+
* ┌─ FPS/MS/MEM graph ─┐ ← always visible (click to cycle)
|
|
38
|
+
* ├─ Stats (folder) ───┤ ← collapsible: draws, tris
|
|
39
|
+
* ├─ ...user controls...┤
|
|
40
|
+
* └────────────────────┘
|
|
41
|
+
*
|
|
42
|
+
* @param options - Title, expansion state, and stats-graph `driver` (see
|
|
43
|
+
* {@link CreatePaneOptions}). Defaults to a self-ticking `'raf'` pane
|
|
44
|
+
* titled "Controls".
|
|
45
|
+
* @returns A {@link PaneBundle} — the Tweakpane `pane` plus an `update()`
|
|
46
|
+
* tick and a `disposed` flag.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const { pane } = createPane({ title: 'Debug' })
|
|
51
|
+
* const folder = pane.addFolder({ title: 'Sprite' })
|
|
52
|
+
* folder.addBinding(state, 'scale', { min: 0.1, max: 4 })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
15
55
|
function createPane(options = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
pane,
|
|
166
|
-
update: () => graph?.update(),
|
|
167
|
-
get disposed() {
|
|
168
|
-
return disposed;
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
_unclaimedPane = bundle;
|
|
172
|
-
return bundle;
|
|
56
|
+
const { title = "Controls", expanded = true, driver = "raf", ...rest } = options;
|
|
57
|
+
if (_unclaimedPane) {
|
|
58
|
+
_unclaimedPane.pane.dispose();
|
|
59
|
+
_unclaimedPane = null;
|
|
60
|
+
}
|
|
61
|
+
const pane = new Pane({
|
|
62
|
+
title,
|
|
63
|
+
expanded,
|
|
64
|
+
...rest
|
|
65
|
+
});
|
|
66
|
+
pane.element.style.zIndex = "1000";
|
|
67
|
+
const wrapper = pane.element.closest(".tp-dfwv");
|
|
68
|
+
if (wrapper && wrapper !== pane.element) wrapper.style.zIndex = "1000";
|
|
69
|
+
applyTheme(pane.element, FLATLAND_THEME);
|
|
70
|
+
registerPlugins(pane);
|
|
71
|
+
pane.element.classList.add("tp-flatland-dimmable");
|
|
72
|
+
const header = pane.element.querySelector(".tp-rotv_b");
|
|
73
|
+
if (header) {
|
|
74
|
+
const pin = document.createElement("span");
|
|
75
|
+
pin.className = "tp-flatland-pin";
|
|
76
|
+
pin.setAttribute("role", "button");
|
|
77
|
+
pin.setAttribute("aria-label", "Keep pane opaque");
|
|
78
|
+
pin.setAttribute("aria-pressed", "false");
|
|
79
|
+
pin.tabIndex = 0;
|
|
80
|
+
pin.title = "Keep pane opaque";
|
|
81
|
+
pin.innerHTML = "<svg width=\"10\" height=\"10\" viewBox=\"0 0 14 14\"><circle cx=\"7\" cy=\"7\" r=\"4\"/></svg>";
|
|
82
|
+
const togglePin = (e) => {
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
const pinned = pane.element.classList.toggle("tp-flatland-pinned");
|
|
86
|
+
pin.setAttribute("aria-pressed", String(pinned));
|
|
87
|
+
};
|
|
88
|
+
pin.addEventListener("click", togglePin);
|
|
89
|
+
pin.addEventListener("mousedown", (e) => {
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
});
|
|
92
|
+
pin.addEventListener("keydown", (e) => {
|
|
93
|
+
if (e.key === "Enter" || e.key === " ") togglePin(e);
|
|
94
|
+
});
|
|
95
|
+
header.insertBefore(pin, header.firstChild);
|
|
96
|
+
}
|
|
97
|
+
let client = null;
|
|
98
|
+
let graph = null;
|
|
99
|
+
let statsRow = null;
|
|
100
|
+
let providerSwitcher = null;
|
|
101
|
+
let registryView = null;
|
|
102
|
+
let buffersView = null;
|
|
103
|
+
let buffersModal = null;
|
|
104
|
+
const ACTIVE_FEATURES = [
|
|
105
|
+
"stats",
|
|
106
|
+
"env",
|
|
107
|
+
"registry",
|
|
108
|
+
"buffers"
|
|
109
|
+
];
|
|
110
|
+
let mode = expanded ? "full" : "collapsed";
|
|
111
|
+
const applyMode = () => {
|
|
112
|
+
pane.element.classList.toggle("tp-flatland-minimal", mode === "minimal");
|
|
113
|
+
if (pane.expanded !== (mode !== "collapsed")) pane.expanded = mode !== "collapsed";
|
|
114
|
+
if (modeToggle !== null) {
|
|
115
|
+
modeToggle.setAttribute("data-mode", mode);
|
|
116
|
+
modeToggle.setAttribute("aria-label", mode === "full" ? "Full pane (click for minimal)" : mode === "minimal" ? "Minimal pane (click to collapse)" : "Collapsed (click to expand)");
|
|
117
|
+
}
|
|
118
|
+
const c = client;
|
|
119
|
+
if (c !== null) if (mode === "full") c.setFeatures(ACTIVE_FEATURES);
|
|
120
|
+
else if (mode === "minimal") c.setFeatures(["stats", "env"]);
|
|
121
|
+
else c.setFeatures([]);
|
|
122
|
+
buffersView?.setPaneFolded(mode !== "full");
|
|
123
|
+
};
|
|
124
|
+
const cycleMode = () => {
|
|
125
|
+
mode = mode === "full" ? "minimal" : mode === "minimal" ? "collapsed" : "full";
|
|
126
|
+
applyMode();
|
|
127
|
+
};
|
|
128
|
+
let modeToggle = null;
|
|
129
|
+
if (header) {
|
|
130
|
+
modeToggle = document.createElement("span");
|
|
131
|
+
modeToggle.className = "tp-flatland-mode";
|
|
132
|
+
modeToggle.setAttribute("role", "button");
|
|
133
|
+
modeToggle.setAttribute("tabindex", "0");
|
|
134
|
+
modeToggle.title = "Cycle pane mode: full → minimal → collapsed";
|
|
135
|
+
modeToggle.innerHTML = "<span class=\"tp-flatland-mode-glyph\"></span>";
|
|
136
|
+
const go = (e) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
cycleMode();
|
|
140
|
+
};
|
|
141
|
+
modeToggle.addEventListener("click", go);
|
|
142
|
+
modeToggle.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
143
|
+
modeToggle.addEventListener("keydown", (e) => {
|
|
144
|
+
if (e.key === "Enter" || e.key === " ") go(e);
|
|
145
|
+
});
|
|
146
|
+
header.appendChild(modeToggle);
|
|
147
|
+
const onHeaderClick = (e) => {
|
|
148
|
+
const target = e.target;
|
|
149
|
+
if (target === null) return;
|
|
150
|
+
if (target.closest(".tp-flatland-pin, .tp-flatland-mode") !== null) return;
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
cycleMode();
|
|
154
|
+
};
|
|
155
|
+
header.addEventListener("click", onHeaderClick, true);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
client = new DevtoolsClient({ features: ACTIVE_FEATURES });
|
|
159
|
+
graph = addStatsGraph(pane, client, { driver });
|
|
160
|
+
statsRow = addStatsRow(pane, client);
|
|
161
|
+
providerSwitcher = addProviderSwitcher(pane, client);
|
|
162
|
+
registryView = addRegistryView(pane, client);
|
|
163
|
+
buffersModal = createBuffersModal(client, {
|
|
164
|
+
onActiveChange: (name) => buffersView?.setActiveFromModal(name),
|
|
165
|
+
onOpen: () => buffersView?.setModalOpen(true),
|
|
166
|
+
onClose: () => buffersView?.setModalOpen(false)
|
|
167
|
+
});
|
|
168
|
+
buffersView = addBuffersView(pane, client, { onExpand: (name) => buffersModal?.open(name) });
|
|
169
|
+
client.start();
|
|
170
|
+
graph?.element.classList.add("tp-flatland-minimal-keep");
|
|
171
|
+
statsRow?.element.classList.add("tp-flatland-minimal-keep");
|
|
172
|
+
applyMode();
|
|
173
|
+
pane.on("fold", (ev) => {
|
|
174
|
+
const desired = ev.expanded ? mode === "collapsed" ? "full" : mode : "collapsed";
|
|
175
|
+
if (desired !== mode) {
|
|
176
|
+
mode = desired;
|
|
177
|
+
applyMode();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} catch {}
|
|
181
|
+
let disposed = false;
|
|
182
|
+
const originalDispose = pane.dispose.bind(pane);
|
|
183
|
+
pane.dispose = () => {
|
|
184
|
+
if (disposed) return;
|
|
185
|
+
disposed = true;
|
|
186
|
+
graph?.dispose();
|
|
187
|
+
statsRow?.dispose();
|
|
188
|
+
providerSwitcher?.dispose();
|
|
189
|
+
registryView?.dispose();
|
|
190
|
+
buffersView?.dispose();
|
|
191
|
+
buffersModal?.dispose();
|
|
192
|
+
client?.dispose();
|
|
193
|
+
if (_unclaimedPane === bundle) _unclaimedPane = null;
|
|
194
|
+
originalDispose();
|
|
195
|
+
};
|
|
196
|
+
const bundle = {
|
|
197
|
+
pane,
|
|
198
|
+
update: () => graph?.update(),
|
|
199
|
+
get disposed() {
|
|
200
|
+
return disposed;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
_unclaimedPane = bundle;
|
|
204
|
+
return bundle;
|
|
173
205
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
};
|
|
206
|
+
//#endregion
|
|
207
|
+
export { claimPane, createPane };
|
|
208
|
+
|
|
178
209
|
//# sourceMappingURL=create-pane.js.map
|
package/dist/create-pane.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/create-pane.ts"],"sourcesContent":["import { Pane } from 'tweakpane'\nimport { applyTheme, FLATLAND_THEME } from './theme.js'\nimport { registerPlugins } from './plugins.js'\nimport { addStatsGraph, type StatsGraphHandle } from './stats-graph.js'\nimport { addStatsRow, type StatsRowHandle } from './stats-row.js'\nimport { addProviderSwitcher, type ProviderSwitcherHandle } from './provider-switcher.js'\nimport { addRegistryView, type RegistryViewHandle } from './registry-view.js'\nimport { addBuffersView, type BuffersViewHandle } from './buffers-view.js'\nimport { createBuffersModal, type BuffersModalHandle } from './buffers-modal.js'\nimport { DevtoolsClient } from './devtools-client.js'\n\nexport interface CreatePaneOptions {\n /** Custom container element */\n container?: HTMLElement\n /** Pane title (default: 'Controls') */\n title?: string\n /** Default expansion state (default: true) */\n expanded?: boolean\n /**\n * Who drives the stats-graph render?\n * - `'raf'` (default): the pane starts its own `requestAnimationFrame`\n * loop. Zero setup; fine for simple apps.\n * - `'manual'`: no internal rAF. Call `bundle.update()` from your own\n * frame tick — `renderer.setAnimationLoop` callback, R3F `useFrame`,\n * or any ticker you already own. Preferred when you have a render\n * loop; avoids a second rAF (which Safari throttles).\n */\n driver?: 'raf' | 'manual'\n}\n\nexport interface PaneBundle {\n pane: Pane\n /**\n * Render a stats-graph frame. Call from your existing animation loop\n * (R3F `useFrame`, `renderer.setAnimationLoop`, etc.) for shared-tick\n * visuals — avoids a second `requestAnimationFrame` callback, which\n * Safari penalises.\n *\n * If you never call `update()`, the graph falls back to its own rAF\n * so the pane works out-of-the-box in apps without a render loop.\n */\n update(): void\n /**\n * `true` once `pane.dispose()` has been called. Lets React hooks\n * detect that a prior cleanup tore the bundle down and decide\n * whether to recreate (StrictMode mount→cleanup→mount).\n */\n readonly disposed: boolean\n}\n\n/**\n * Module-level \"unclaimed pane\" slot.\n *\n * React 18+ with StrictMode and Suspense can render a component multiple\n * times before the first effect commits (each aborted render discards its\n * `useRef` slots, including `bundleRef`). Because `new Pane()` mounts a\n * wrapper element to `document.body` synchronously, every aborted attempt\n * would leak a pane into the DOM (each with its own RAF loop).\n *\n * This slot tracks the most recent pane that hasn't been \"claimed\" by a\n * committed `useEffect`. When `createPane` is called again, any still-\n * unclaimed pane is disposed first — cleaning up orphans from the previous\n * discarded render. Once `usePane`'s effect commits, it calls `claimPane`\n * to prevent the current pane from being disposed by future `createPane`\n * calls in unrelated components.\n */\nlet _unclaimedPane: PaneBundle | null = null\n\nexport function claimPane(bundle: PaneBundle): void {\n if (_unclaimedPane === bundle) _unclaimedPane = null\n}\n\n/**\n * Create a themed Tweakpane instance with collapsible header and stats.\n *\n * Layout:\n * [Controls ▾] ← collapsible pane header\n * ┌─ FPS/MS/MEM graph ─┐ ← always visible (click to cycle)\n * ├─ Stats (folder) ───┤ ← collapsible: draws, tris\n * ├─ ...user controls...┤\n * └────────────────────┘\n *\n * @param options - Title, expansion state, and stats-graph `driver` (see\n * {@link CreatePaneOptions}). Defaults to a self-ticking `'raf'` pane\n * titled \"Controls\".\n * @returns A {@link PaneBundle} — the Tweakpane `pane` plus an `update()`\n * tick and a `disposed` flag.\n *\n * @example\n * ```typescript\n * const { pane } = createPane({ title: 'Debug' })\n * const folder = pane.addFolder({ title: 'Sprite' })\n * folder.addBinding(state, 'scale', { min: 0.1, max: 4 })\n * ```\n */\nexport function createPane(options: CreatePaneOptions = {}): PaneBundle {\n const {\n title = 'Controls',\n expanded = true,\n driver = 'raf',\n ...rest\n } = options\n\n // Dispose any orphaned pane from a previous (aborted) render attempt.\n if (_unclaimedPane) {\n _unclaimedPane.pane.dispose()\n _unclaimedPane = null\n }\n\n const pane = new Pane({ title, expanded, ...rest })\n\n // Ensure pane floats above canvas elements (R3F creates full-viewport divs).\n // Tweakpane wraps `pane.element` in a `.tp-dfwv` default-wrapper that's the\n // actual body sibling and stacks against other overlays — z-index on the\n // inner element would be a no-op. Apply to the wrapper when it exists.\n pane.element.style.zIndex = '1000'\n const wrapper = pane.element.closest<HTMLElement>('.tp-dfwv')\n if (wrapper && wrapper !== pane.element) {\n wrapper.style.zIndex = '1000'\n }\n\n applyTheme(pane.element, FLATLAND_THEME)\n registerPlugins(pane)\n\n // Idle-dimming + pin toggle. The pane is translucent while the mouse\n // isn't over it, fully opaque on hover. Clicking the pin in the header\n // locks it to fully opaque regardless of hover. See theme.ts for the\n // CSS that backs the `tp-flatland-dimmable` / `tp-flatland-pinned`\n // classes and the `tp-flatland-pin` element.\n pane.element.classList.add('tp-flatland-dimmable')\n const header = pane.element.querySelector<HTMLElement>('.tp-rotv_b')\n if (header) {\n const pin = document.createElement('span')\n pin.className = 'tp-flatland-pin'\n pin.setAttribute('role', 'button')\n pin.setAttribute('aria-label', 'Keep pane opaque')\n pin.setAttribute('aria-pressed', 'false')\n pin.tabIndex = 0\n pin.title = 'Keep pane opaque'\n pin.innerHTML = '<svg width=\"10\" height=\"10\" viewBox=\"0 0 14 14\"><circle cx=\"7\" cy=\"7\" r=\"4\"/></svg>'\n\n const togglePin = (e: Event) => {\n e.stopPropagation()\n e.preventDefault()\n const pinned = pane.element.classList.toggle('tp-flatland-pinned')\n pin.setAttribute('aria-pressed', String(pinned))\n }\n pin.addEventListener('click', togglePin)\n // Stop mousedown so the header button doesn't flash its :active state\n // (and doesn't gain focus) when clicking the pin.\n pin.addEventListener('mousedown', (e) => {\n e.stopPropagation()\n })\n pin.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') togglePin(e)\n })\n\n header.insertBefore(pin, header.firstChild)\n }\n\n // Always-on stats graph + row, bus-driven. A single `DevtoolsClient`\n // feeds both. If the bus isn't available in the environment (e.g., a\n // test runner without BroadcastChannel), the try/catch swallows it\n // and the pane still works for user controls — just no stats display.\n let client: DevtoolsClient | null = null\n let graph: StatsGraphHandle | null = null\n let statsRow: StatsRowHandle | null = null\n let providerSwitcher: ProviderSwitcherHandle | null = null\n let registryView: RegistryViewHandle | null = null\n let buffersView: BuffersViewHandle | null = null\n let buffersModal: BuffersModalHandle | null = null\n\n const ACTIVE_FEATURES: ('stats' | 'env' | 'registry' | 'buffers')[] = ['stats', 'env', 'registry', 'buffers']\n\n // Three-mode cycle — full → minimal → collapsed → full. Replaces\n // Tweakpane's binary fold with something more useful: \"minimal\" keeps\n // only the stats graph + draws/tris row visible (no buffers, no\n // registry, no provider switcher, no user-added controls) so you can\n // watch perf numbers without a giant pane covering the viewport, AND\n // without paying the cost of buffer streaming.\n type Mode = 'full' | 'minimal' | 'collapsed'\n let mode: Mode = expanded ? 'full' : 'collapsed'\n\n const applyMode = (): void => {\n pane.element.classList.toggle('tp-flatland-minimal', mode === 'minimal')\n // 'full' and 'minimal' both have the pane expanded at the TP level;\n // 'collapsed' folds it.\n if (pane.expanded !== (mode !== 'collapsed')) {\n pane.expanded = mode !== 'collapsed'\n }\n // Our own caret icon reflects the cycle position.\n if (modeToggle !== null) {\n modeToggle.setAttribute('data-mode', mode)\n modeToggle.setAttribute(\n 'aria-label',\n mode === 'full' ? 'Full pane (click for minimal)'\n : mode === 'minimal' ? 'Minimal pane (click to collapse)'\n : 'Collapsed (click to expand)',\n )\n }\n const c = client\n if (c !== null) {\n // Feature gating follows the mode:\n // full → everything\n // minimal → stats + env only; no registry, no buffers drain\n // collapsed → nothing\n if (mode === 'full') c.setFeatures(ACTIVE_FEATURES)\n else if (mode === 'minimal') c.setFeatures(['stats', 'env'])\n else c.setFeatures([])\n }\n // Propagate into the buffers view so it knows to unsubscribe. Both\n // 'minimal' and 'collapsed' ask the view to treat itself as folded\n // (no thumbnail stream, no GPU readback on the provider).\n buffersView?.setPaneFolded(mode !== 'full')\n }\n\n const cycleMode = (): void => {\n mode = mode === 'full' ? 'minimal' : mode === 'minimal' ? 'collapsed' : 'full'\n applyMode()\n }\n\n // Replace Tweakpane's built-in fold caret with our own mode-cycle\n // button AND intercept clicks anywhere on the pane header so the\n // native binary-fold behavior doesn't skip 'minimal'. The native\n // caret is hidden via CSS (.tp-rotv_m display:none).\n let modeToggle: HTMLElement | null = null\n if (header) {\n modeToggle = document.createElement('span')\n modeToggle.className = 'tp-flatland-mode'\n modeToggle.setAttribute('role', 'button')\n modeToggle.setAttribute('tabindex', '0')\n modeToggle.title = 'Cycle pane mode: full → minimal → collapsed'\n // Single caret glyph — CSS rotates it based on data-mode so one\n // visual vocabulary handles all three states. Matches tweakpane's\n // native rotating-caret convention and gives a screw-head look at\n // 45° for minimal.\n modeToggle.innerHTML = '<span class=\"tp-flatland-mode-glyph\"></span>'\n const go = (e: Event): void => {\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n modeToggle.addEventListener('click', go)\n modeToggle.addEventListener('mousedown', (e) => e.stopPropagation())\n modeToggle.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') go(e)\n })\n header.appendChild(modeToggle)\n\n // Capture-phase click on the header itself — Tweakpane's root\n // component wires a listener that toggles `expanded`, which would\n // skip our tri-state. Intercept before it fires, cycle our modes,\n // stop propagation so the default toggle never runs. Clicks on our\n // own overlay buttons (pin, mode toggle) already stop propagation\n // themselves, so they're unaffected.\n const onHeaderClick = (e: MouseEvent): void => {\n const target = e.target as HTMLElement | null\n if (target === null) return\n // Let clicks on our own interactive children through untouched.\n if (target.closest('.tp-flatland-pin, .tp-flatland-mode') !== null) return\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n header.addEventListener('click', onHeaderClick, true)\n }\n\n try {\n client = new DevtoolsClient({ features: ACTIVE_FEATURES })\n graph = addStatsGraph(pane, client, { driver })\n statsRow = addStatsRow(pane, client)\n // Hidden until two or more providers announce.\n providerSwitcher = addProviderSwitcher(pane, client)\n // Hidden until the provider publishes at least one entry via\n // `registerDebugArray`.\n registryView = addRegistryView(pane, client)\n // Modal is built lazily — `createBuffersModal` mounts the DOM\n // immediately (display:none) so the first expand is instant.\n buffersModal = createBuffersModal(client, {\n onActiveChange: (name) => buffersView?.setActiveFromModal(name),\n onOpen: () => buffersView?.setModalOpen(true),\n onClose: () => buffersView?.setModalOpen(false),\n })\n // Hidden until the provider publishes at least one debug texture\n // via `registerDebugTexture`. ⤢ in-blade button calls into the\n // modal handle.\n buffersView = addBuffersView(pane, client, {\n onExpand: (name) => buffersModal?.open(name),\n })\n client.start()\n\n // Mark the always-visible blades so CSS can keep them shown while\n // hiding everything else in 'minimal' mode.\n graph?.element.classList.add('tp-flatland-minimal-keep')\n statsRow?.element.classList.add('tp-flatland-minimal-keep')\n\n applyMode()\n\n // Tweakpane still tries to toggle expanded when the user clicks the\n // header — intercept and redirect through our cycle. The `fold`\n // event fires from keyboard ⇥-space or external sets; keep it as a\n // fallback that syncs our mode if something else flips expansion.\n pane.on('fold', (ev) => {\n const desired: Mode = ev.expanded\n ? (mode === 'collapsed' ? 'full' : mode)\n : 'collapsed'\n if (desired !== mode) {\n mode = desired\n applyMode()\n }\n })\n } catch {\n // Bus unavailable — skip the stats blades. Pane still usable.\n }\n\n // Clean up on dispose. Idempotent: calling `dispose()` twice (e.g.,\n // once by the user and once by a later `createPane` disposing the\n // unclaimed slot) no-ops the second time instead of throwing.\n let disposed = false\n const originalDispose = pane.dispose.bind(pane)\n pane.dispose = () => {\n if (disposed) return\n disposed = true\n graph?.dispose()\n statsRow?.dispose()\n providerSwitcher?.dispose()\n registryView?.dispose()\n buffersView?.dispose()\n buffersModal?.dispose()\n client?.dispose()\n // If the user disposed the pane directly without claiming, free\n // the slot so the next createPane doesn't try to dispose us again.\n if (_unclaimedPane === bundle) _unclaimedPane = null\n originalDispose()\n }\n\n const bundle: PaneBundle = {\n pane,\n update: () => graph?.update(),\n get disposed() {\n return disposed\n },\n }\n _unclaimedPane = bundle\n return bundle\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"create-pane.js","names":[],"sources":["../src/create-pane.ts"],"sourcesContent":["import { Pane } from 'tweakpane'\nimport { applyTheme, FLATLAND_THEME } from './theme.js'\nimport { registerPlugins } from './plugins.js'\nimport { addStatsGraph, type StatsGraphHandle } from './stats-graph.js'\nimport { addStatsRow, type StatsRowHandle } from './stats-row.js'\nimport { addProviderSwitcher, type ProviderSwitcherHandle } from './provider-switcher.js'\nimport { addRegistryView, type RegistryViewHandle } from './registry-view.js'\nimport { addBuffersView, type BuffersViewHandle } from './buffers-view.js'\nimport { createBuffersModal, type BuffersModalHandle } from './buffers-modal.js'\nimport { DevtoolsClient } from './devtools-client.js'\n\nexport interface CreatePaneOptions {\n /** Custom container element */\n container?: HTMLElement\n /** Pane title (default: 'Controls') */\n title?: string\n /** Default expansion state (default: true) */\n expanded?: boolean\n /**\n * Who drives the stats-graph render?\n * - `'raf'` (default): the pane starts its own `requestAnimationFrame`\n * loop. Zero setup; fine for simple apps.\n * - `'manual'`: no internal rAF. Call `bundle.update()` from your own\n * frame tick — `renderer.setAnimationLoop` callback, R3F `useFrame`,\n * or any ticker you already own. Preferred when you have a render\n * loop; avoids a second rAF (which Safari throttles).\n */\n driver?: 'raf' | 'manual'\n}\n\nexport interface PaneBundle {\n pane: Pane\n /**\n * Render a stats-graph frame. Call from your existing animation loop\n * (R3F `useFrame`, `renderer.setAnimationLoop`, etc.) for shared-tick\n * visuals — avoids a second `requestAnimationFrame` callback, which\n * Safari penalises.\n *\n * If you never call `update()`, the graph falls back to its own rAF\n * so the pane works out-of-the-box in apps without a render loop.\n */\n update(): void\n /**\n * `true` once `pane.dispose()` has been called. Lets React hooks\n * detect that a prior cleanup tore the bundle down and decide\n * whether to recreate (StrictMode mount→cleanup→mount).\n */\n readonly disposed: boolean\n}\n\n/**\n * Module-level \"unclaimed pane\" slot.\n *\n * React 18+ with StrictMode and Suspense can render a component multiple\n * times before the first effect commits (each aborted render discards its\n * `useRef` slots, including `bundleRef`). Because `new Pane()` mounts a\n * wrapper element to `document.body` synchronously, every aborted attempt\n * would leak a pane into the DOM (each with its own RAF loop).\n *\n * This slot tracks the most recent pane that hasn't been \"claimed\" by a\n * committed `useEffect`. When `createPane` is called again, any still-\n * unclaimed pane is disposed first — cleaning up orphans from the previous\n * discarded render. Once `usePane`'s effect commits, it calls `claimPane`\n * to prevent the current pane from being disposed by future `createPane`\n * calls in unrelated components.\n */\nlet _unclaimedPane: PaneBundle | null = null\n\nexport function claimPane(bundle: PaneBundle): void {\n if (_unclaimedPane === bundle) _unclaimedPane = null\n}\n\n/**\n * Create a themed Tweakpane instance with collapsible header and stats.\n *\n * Layout:\n * [Controls ▾] ← collapsible pane header\n * ┌─ FPS/MS/MEM graph ─┐ ← always visible (click to cycle)\n * ├─ Stats (folder) ───┤ ← collapsible: draws, tris\n * ├─ ...user controls...┤\n * └────────────────────┘\n *\n * @param options - Title, expansion state, and stats-graph `driver` (see\n * {@link CreatePaneOptions}). Defaults to a self-ticking `'raf'` pane\n * titled \"Controls\".\n * @returns A {@link PaneBundle} — the Tweakpane `pane` plus an `update()`\n * tick and a `disposed` flag.\n *\n * @example\n * ```typescript\n * const { pane } = createPane({ title: 'Debug' })\n * const folder = pane.addFolder({ title: 'Sprite' })\n * folder.addBinding(state, 'scale', { min: 0.1, max: 4 })\n * ```\n */\nexport function createPane(options: CreatePaneOptions = {}): PaneBundle {\n const { title = 'Controls', expanded = true, driver = 'raf', ...rest } = options\n\n // Dispose any orphaned pane from a previous (aborted) render attempt.\n if (_unclaimedPane) {\n _unclaimedPane.pane.dispose()\n _unclaimedPane = null\n }\n\n const pane = new Pane({ title, expanded, ...rest })\n\n // Ensure pane floats above canvas elements (R3F creates full-viewport divs).\n // Tweakpane wraps `pane.element` in a `.tp-dfwv` default-wrapper that's the\n // actual body sibling and stacks against other overlays — z-index on the\n // inner element would be a no-op. Apply to the wrapper when it exists.\n pane.element.style.zIndex = '1000'\n const wrapper = pane.element.closest<HTMLElement>('.tp-dfwv')\n if (wrapper && wrapper !== pane.element) {\n wrapper.style.zIndex = '1000'\n }\n\n applyTheme(pane.element, FLATLAND_THEME)\n registerPlugins(pane)\n\n // Idle-dimming + pin toggle. The pane is translucent while the mouse\n // isn't over it, fully opaque on hover. Clicking the pin in the header\n // locks it to fully opaque regardless of hover. See theme.ts for the\n // CSS that backs the `tp-flatland-dimmable` / `tp-flatland-pinned`\n // classes and the `tp-flatland-pin` element.\n pane.element.classList.add('tp-flatland-dimmable')\n const header = pane.element.querySelector<HTMLElement>('.tp-rotv_b')\n if (header) {\n const pin = document.createElement('span')\n pin.className = 'tp-flatland-pin'\n pin.setAttribute('role', 'button')\n pin.setAttribute('aria-label', 'Keep pane opaque')\n pin.setAttribute('aria-pressed', 'false')\n pin.tabIndex = 0\n pin.title = 'Keep pane opaque'\n pin.innerHTML = '<svg width=\"10\" height=\"10\" viewBox=\"0 0 14 14\"><circle cx=\"7\" cy=\"7\" r=\"4\"/></svg>'\n\n const togglePin = (e: Event) => {\n e.stopPropagation()\n e.preventDefault()\n const pinned = pane.element.classList.toggle('tp-flatland-pinned')\n pin.setAttribute('aria-pressed', String(pinned))\n }\n pin.addEventListener('click', togglePin)\n // Stop mousedown so the header button doesn't flash its :active state\n // (and doesn't gain focus) when clicking the pin.\n pin.addEventListener('mousedown', (e) => {\n e.stopPropagation()\n })\n pin.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') togglePin(e)\n })\n\n header.insertBefore(pin, header.firstChild)\n }\n\n // Always-on stats graph + row, bus-driven. A single `DevtoolsClient`\n // feeds both. If the bus isn't available in the environment (e.g., a\n // test runner without BroadcastChannel), the try/catch swallows it\n // and the pane still works for user controls — just no stats display.\n let client: DevtoolsClient | null = null\n let graph: StatsGraphHandle | null = null\n let statsRow: StatsRowHandle | null = null\n let providerSwitcher: ProviderSwitcherHandle | null = null\n let registryView: RegistryViewHandle | null = null\n let buffersView: BuffersViewHandle | null = null\n let buffersModal: BuffersModalHandle | null = null\n\n const ACTIVE_FEATURES: ('stats' | 'env' | 'registry' | 'buffers')[] = ['stats', 'env', 'registry', 'buffers']\n\n // Three-mode cycle — full → minimal → collapsed → full. Replaces\n // Tweakpane's binary fold with something more useful: \"minimal\" keeps\n // only the stats graph + draws/tris row visible (no buffers, no\n // registry, no provider switcher, no user-added controls) so you can\n // watch perf numbers without a giant pane covering the viewport, AND\n // without paying the cost of buffer streaming.\n type Mode = 'full' | 'minimal' | 'collapsed'\n let mode: Mode = expanded ? 'full' : 'collapsed'\n\n const applyMode = (): void => {\n pane.element.classList.toggle('tp-flatland-minimal', mode === 'minimal')\n // 'full' and 'minimal' both have the pane expanded at the TP level;\n // 'collapsed' folds it.\n if (pane.expanded !== (mode !== 'collapsed')) {\n pane.expanded = mode !== 'collapsed'\n }\n // Our own caret icon reflects the cycle position.\n if (modeToggle !== null) {\n modeToggle.setAttribute('data-mode', mode)\n modeToggle.setAttribute(\n 'aria-label',\n mode === 'full'\n ? 'Full pane (click for minimal)'\n : mode === 'minimal'\n ? 'Minimal pane (click to collapse)'\n : 'Collapsed (click to expand)'\n )\n }\n const c = client\n if (c !== null) {\n // Feature gating follows the mode:\n // full → everything\n // minimal → stats + env only; no registry, no buffers drain\n // collapsed → nothing\n if (mode === 'full') c.setFeatures(ACTIVE_FEATURES)\n else if (mode === 'minimal') c.setFeatures(['stats', 'env'])\n else c.setFeatures([])\n }\n // Propagate into the buffers view so it knows to unsubscribe. Both\n // 'minimal' and 'collapsed' ask the view to treat itself as folded\n // (no thumbnail stream, no GPU readback on the provider).\n buffersView?.setPaneFolded(mode !== 'full')\n }\n\n const cycleMode = (): void => {\n mode = mode === 'full' ? 'minimal' : mode === 'minimal' ? 'collapsed' : 'full'\n applyMode()\n }\n\n // Replace Tweakpane's built-in fold caret with our own mode-cycle\n // button AND intercept clicks anywhere on the pane header so the\n // native binary-fold behavior doesn't skip 'minimal'. The native\n // caret is hidden via CSS (.tp-rotv_m display:none).\n let modeToggle: HTMLElement | null = null\n if (header) {\n modeToggle = document.createElement('span')\n modeToggle.className = 'tp-flatland-mode'\n modeToggle.setAttribute('role', 'button')\n modeToggle.setAttribute('tabindex', '0')\n modeToggle.title = 'Cycle pane mode: full → minimal → collapsed'\n // Single caret glyph — CSS rotates it based on data-mode so one\n // visual vocabulary handles all three states. Matches tweakpane's\n // native rotating-caret convention and gives a screw-head look at\n // 45° for minimal.\n modeToggle.innerHTML = '<span class=\"tp-flatland-mode-glyph\"></span>'\n const go = (e: Event): void => {\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n modeToggle.addEventListener('click', go)\n modeToggle.addEventListener('mousedown', (e) => e.stopPropagation())\n modeToggle.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') go(e)\n })\n header.appendChild(modeToggle)\n\n // Capture-phase click on the header itself — Tweakpane's root\n // component wires a listener that toggles `expanded`, which would\n // skip our tri-state. Intercept before it fires, cycle our modes,\n // stop propagation so the default toggle never runs. Clicks on our\n // own overlay buttons (pin, mode toggle) already stop propagation\n // themselves, so they're unaffected.\n const onHeaderClick = (e: MouseEvent): void => {\n const target = e.target as HTMLElement | null\n if (target === null) return\n // Let clicks on our own interactive children through untouched.\n if (target.closest('.tp-flatland-pin, .tp-flatland-mode') !== null) return\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n header.addEventListener('click', onHeaderClick, true)\n }\n\n try {\n client = new DevtoolsClient({ features: ACTIVE_FEATURES })\n graph = addStatsGraph(pane, client, { driver })\n statsRow = addStatsRow(pane, client)\n // Hidden until two or more providers announce.\n providerSwitcher = addProviderSwitcher(pane, client)\n // Hidden until the provider publishes at least one entry via\n // `registerDebugArray`.\n registryView = addRegistryView(pane, client)\n // Modal is built lazily — `createBuffersModal` mounts the DOM\n // immediately (display:none) so the first expand is instant.\n buffersModal = createBuffersModal(client, {\n onActiveChange: (name) => buffersView?.setActiveFromModal(name),\n onOpen: () => buffersView?.setModalOpen(true),\n onClose: () => buffersView?.setModalOpen(false),\n })\n // Hidden until the provider publishes at least one debug texture\n // via `registerDebugTexture`. ⤢ in-blade button calls into the\n // modal handle.\n buffersView = addBuffersView(pane, client, {\n onExpand: (name) => buffersModal?.open(name),\n })\n client.start()\n\n // Mark the always-visible blades so CSS can keep them shown while\n // hiding everything else in 'minimal' mode.\n graph?.element.classList.add('tp-flatland-minimal-keep')\n statsRow?.element.classList.add('tp-flatland-minimal-keep')\n\n applyMode()\n\n // Tweakpane still tries to toggle expanded when the user clicks the\n // header — intercept and redirect through our cycle. The `fold`\n // event fires from keyboard ⇥-space or external sets; keep it as a\n // fallback that syncs our mode if something else flips expansion.\n pane.on('fold', (ev) => {\n const desired: Mode = ev.expanded ? (mode === 'collapsed' ? 'full' : mode) : 'collapsed'\n if (desired !== mode) {\n mode = desired\n applyMode()\n }\n })\n } catch {\n // Bus unavailable — skip the stats blades. Pane still usable.\n }\n\n // Clean up on dispose. Idempotent: calling `dispose()` twice (e.g.,\n // once by the user and once by a later `createPane` disposing the\n // unclaimed slot) no-ops the second time instead of throwing.\n let disposed = false\n const originalDispose = pane.dispose.bind(pane)\n pane.dispose = () => {\n if (disposed) return\n disposed = true\n graph?.dispose()\n statsRow?.dispose()\n providerSwitcher?.dispose()\n registryView?.dispose()\n buffersView?.dispose()\n buffersModal?.dispose()\n client?.dispose()\n // If the user disposed the pane directly without claiming, free\n // the slot so the next createPane doesn't try to dispose us again.\n if (_unclaimedPane === bundle) _unclaimedPane = null\n originalDispose()\n }\n\n const bundle: PaneBundle = {\n pane,\n update: () => graph?.update(),\n get disposed() {\n return disposed\n },\n }\n _unclaimedPane = bundle\n return bundle\n}\n"],"mappings":"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"}
|
package/dist/dashboard/app.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { HeaderStats } from './panels/header-stats.js'
|
|
|
11
11
|
import { ProducerSelect } from './panels/producer-select.js'
|
|
12
12
|
import { ProtocolLog } from './panels/protocol-log.js'
|
|
13
13
|
import { RegistryPanel } from './panels/registry.js'
|
|
14
|
+
import { Scrubber } from './panels/scrubber.js'
|
|
14
15
|
import { StatsStrip } from './panels/stats.js'
|
|
15
16
|
|
|
16
17
|
export function App() {
|
|
@@ -25,6 +26,7 @@ export function App() {
|
|
|
25
26
|
<main class="dashboard-main">
|
|
26
27
|
<section class="dashboard-content">
|
|
27
28
|
<StatsStrip />
|
|
29
|
+
<Scrubber />
|
|
28
30
|
<div class="dashboard-split">
|
|
29
31
|
<div class="dashboard-split-col">
|
|
30
32
|
<BuffersPanel />
|
package/dist/dashboard/export.ts
CHANGED
|
@@ -74,9 +74,7 @@ export async function exportSession(providerId: string | null = null): Promise<v
|
|
|
74
74
|
const a = document.createElement('a')
|
|
75
75
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
76
76
|
a.href = url
|
|
77
|
-
a.download = providerId !== null
|
|
78
|
-
? `tf-devtools-${providerId}-${stamp}.json`
|
|
79
|
-
: `tf-devtools-session-${stamp}.json`
|
|
77
|
+
a.download = providerId !== null ? `tf-devtools-${providerId}-${stamp}.json` : `tf-devtools-session-${stamp}.json`
|
|
80
78
|
document.body.appendChild(a)
|
|
81
79
|
a.click()
|
|
82
80
|
document.body.removeChild(a)
|