@three-flatland/devtools 0.1.0-alpha.3

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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/buffers-modal.cjs +491 -0
  4. package/dist/buffers-modal.cjs.map +1 -0
  5. package/dist/buffers-modal.d.cts +62 -0
  6. package/dist/buffers-modal.d.ts +62 -0
  7. package/dist/buffers-modal.js +467 -0
  8. package/dist/buffers-modal.js.map +1 -0
  9. package/dist/buffers-view.cjs +269 -0
  10. package/dist/buffers-view.cjs.map +1 -0
  11. package/dist/buffers-view.d.cts +39 -0
  12. package/dist/buffers-view.d.ts +39 -0
  13. package/dist/buffers-view.js +245 -0
  14. package/dist/buffers-view.js.map +1 -0
  15. package/dist/create-pane.cjs +203 -0
  16. package/dist/create-pane.cjs.map +1 -0
  17. package/dist/create-pane.d.cts +66 -0
  18. package/dist/create-pane.d.ts +66 -0
  19. package/dist/create-pane.js +178 -0
  20. package/dist/create-pane.js.map +1 -0
  21. package/dist/dashboard/app.tsx +42 -0
  22. package/dist/dashboard/client.ts +21 -0
  23. package/dist/dashboard/export.ts +84 -0
  24. package/dist/dashboard/hooks.ts +75 -0
  25. package/dist/dashboard/index.html +1149 -0
  26. package/dist/dashboard/index.tsx +18 -0
  27. package/dist/dashboard/panels/batches.tsx +282 -0
  28. package/dist/dashboard/panels/buffers.tsx +425 -0
  29. package/dist/dashboard/panels/env.tsx +85 -0
  30. package/dist/dashboard/panels/header-stats.tsx +30 -0
  31. package/dist/dashboard/panels/producer-select.tsx +81 -0
  32. package/dist/dashboard/panels/protocol-log.tsx +743 -0
  33. package/dist/dashboard/panels/registry.tsx +642 -0
  34. package/dist/dashboard/panels/stats.tsx +431 -0
  35. package/dist/dashboard/protocol-store.ts +312 -0
  36. package/dist/dashboard/tsconfig.json +19 -0
  37. package/dist/dashboard/vendor/hooks.module.js +1 -0
  38. package/dist/dashboard/vendor/jsx-runtime.js +22 -0
  39. package/dist/dashboard/vendor/preact.module.js +12 -0
  40. package/dist/devtools-client.cjs +870 -0
  41. package/dist/devtools-client.cjs.map +1 -0
  42. package/dist/devtools-client.d.cts +362 -0
  43. package/dist/devtools-client.d.ts +362 -0
  44. package/dist/devtools-client.js +852 -0
  45. package/dist/devtools-client.js.map +1 -0
  46. package/dist/index.cjs +42 -0
  47. package/dist/index.cjs.map +1 -0
  48. package/dist/index.d.cts +8 -0
  49. package/dist/index.d.ts +8 -0
  50. package/dist/index.js +13 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/perf-trace.cjs +103 -0
  53. package/dist/perf-trace.cjs.map +1 -0
  54. package/dist/perf-trace.d.cts +27 -0
  55. package/dist/perf-trace.d.ts +27 -0
  56. package/dist/perf-trace.js +79 -0
  57. package/dist/perf-trace.js.map +1 -0
  58. package/dist/plugins.cjs +44 -0
  59. package/dist/plugins.cjs.map +1 -0
  60. package/dist/plugins.d.cts +7 -0
  61. package/dist/plugins.d.ts +7 -0
  62. package/dist/plugins.js +9 -0
  63. package/dist/plugins.js.map +1 -0
  64. package/dist/provider-switcher.cjs +95 -0
  65. package/dist/provider-switcher.cjs.map +1 -0
  66. package/dist/provider-switcher.d.cts +18 -0
  67. package/dist/provider-switcher.d.ts +18 -0
  68. package/dist/provider-switcher.js +71 -0
  69. package/dist/provider-switcher.js.map +1 -0
  70. package/dist/react/devtools-provider.cjs +59 -0
  71. package/dist/react/devtools-provider.cjs.map +1 -0
  72. package/dist/react/devtools-provider.d.cts +39 -0
  73. package/dist/react/devtools-provider.d.ts +39 -0
  74. package/dist/react/devtools-provider.js +38 -0
  75. package/dist/react/devtools-provider.js.map +1 -0
  76. package/dist/react/use-fps-graph.cjs +49 -0
  77. package/dist/react/use-fps-graph.cjs.map +1 -0
  78. package/dist/react/use-fps-graph.d.cts +15 -0
  79. package/dist/react/use-fps-graph.d.ts +15 -0
  80. package/dist/react/use-fps-graph.js +25 -0
  81. package/dist/react/use-fps-graph.js.map +1 -0
  82. package/dist/react/use-pane-button.cjs +58 -0
  83. package/dist/react/use-pane-button.cjs.map +1 -0
  84. package/dist/react/use-pane-button.d.cts +10 -0
  85. package/dist/react/use-pane-button.d.ts +10 -0
  86. package/dist/react/use-pane-button.js +34 -0
  87. package/dist/react/use-pane-button.js.map +1 -0
  88. package/dist/react/use-pane-folder.cjs +48 -0
  89. package/dist/react/use-pane-folder.cjs.map +1 -0
  90. package/dist/react/use-pane-folder.d.cts +15 -0
  91. package/dist/react/use-pane-folder.d.ts +15 -0
  92. package/dist/react/use-pane-folder.js +24 -0
  93. package/dist/react/use-pane-folder.js.map +1 -0
  94. package/dist/react/use-pane-input.cjs +70 -0
  95. package/dist/react/use-pane-input.cjs.map +1 -0
  96. package/dist/react/use-pane-input.d.cts +44 -0
  97. package/dist/react/use-pane-input.d.ts +44 -0
  98. package/dist/react/use-pane-input.js +46 -0
  99. package/dist/react/use-pane-input.js.map +1 -0
  100. package/dist/react/use-pane-radio-grid.cjs +78 -0
  101. package/dist/react/use-pane-radio-grid.cjs.map +1 -0
  102. package/dist/react/use-pane-radio-grid.d.cts +35 -0
  103. package/dist/react/use-pane-radio-grid.d.ts +35 -0
  104. package/dist/react/use-pane-radio-grid.js +54 -0
  105. package/dist/react/use-pane-radio-grid.js.map +1 -0
  106. package/dist/react/use-pane.cjs +45 -0
  107. package/dist/react/use-pane.cjs.map +1 -0
  108. package/dist/react/use-pane.d.cts +40 -0
  109. package/dist/react/use-pane.d.ts +40 -0
  110. package/dist/react/use-pane.js +21 -0
  111. package/dist/react/use-pane.js.map +1 -0
  112. package/dist/react.cjs +50 -0
  113. package/dist/react.cjs.map +1 -0
  114. package/dist/react.d.cts +12 -0
  115. package/dist/react.d.ts +12 -0
  116. package/dist/react.js +19 -0
  117. package/dist/react.js.map +1 -0
  118. package/dist/registry-view.cjs +371 -0
  119. package/dist/registry-view.cjs.map +1 -0
  120. package/dist/registry-view.d.cts +24 -0
  121. package/dist/registry-view.d.ts +24 -0
  122. package/dist/registry-view.js +347 -0
  123. package/dist/registry-view.js.map +1 -0
  124. package/dist/stats-graph.cjs +297 -0
  125. package/dist/stats-graph.cjs.map +1 -0
  126. package/dist/stats-graph.d.cts +40 -0
  127. package/dist/stats-graph.d.ts +40 -0
  128. package/dist/stats-graph.js +273 -0
  129. package/dist/stats-graph.js.map +1 -0
  130. package/dist/stats-row.cjs +153 -0
  131. package/dist/stats-row.cjs.map +1 -0
  132. package/dist/stats-row.d.cts +24 -0
  133. package/dist/stats-row.d.ts +24 -0
  134. package/dist/stats-row.js +129 -0
  135. package/dist/stats-row.js.map +1 -0
  136. package/dist/theme.cjs +260 -0
  137. package/dist/theme.cjs.map +1 -0
  138. package/dist/theme.d.cts +10 -0
  139. package/dist/theme.d.ts +10 -0
  140. package/dist/theme.js +235 -0
  141. package/dist/theme.js.map +1 -0
  142. package/dist/vite-plugin.cjs +113 -0
  143. package/dist/vite-plugin.cjs.map +1 -0
  144. package/dist/vite-plugin.d.cts +9 -0
  145. package/dist/vite-plugin.d.ts +9 -0
  146. package/dist/vite-plugin.js +88 -0
  147. package/dist/vite-plugin.js.map +1 -0
  148. package/package.json +124 -0
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var stats_graph_exports = {};
20
+ __export(stats_graph_exports, {
21
+ addStatsGraph: () => addStatsGraph
22
+ });
23
+ module.exports = __toCommonJS(stats_graph_exports);
24
+ var import_debug_protocol = require("three-flatland/debug-protocol");
25
+ const MODES = ["fps", "ms", "gpu", "mem"];
26
+ const BUFFER_SIZE = 80;
27
+ const MODE_COLORS = {
28
+ fps: "#47cca9",
29
+ // retro-cyan
30
+ ms: "#47cc6a",
31
+ // green variant
32
+ gpu: "#ffa347",
33
+ // retro-amber
34
+ mem: "#d94c87"
35
+ // retro-pink
36
+ };
37
+ const MODE_UNITS = {
38
+ fps: "FPS",
39
+ ms: "MS",
40
+ gpu: "MS",
41
+ mem: "MB"
42
+ };
43
+ function addStatsGraph(parent, client, options = {}) {
44
+ const { rows = 2, driver = "raf" } = options;
45
+ let mode = "fps";
46
+ let modeIndex = 0;
47
+ const graphMax = {
48
+ fps: 120,
49
+ ms: 16.67,
50
+ gpu: 16.67,
51
+ mem: 256
52
+ };
53
+ const sessionMin = { fps: Infinity, ms: Infinity, gpu: Infinity, mem: Infinity };
54
+ const sessionMax = { fps: 0, ms: 0, gpu: 0, mem: 0 };
55
+ const lblv = document.createElement("div");
56
+ lblv.className = "tp-lblv";
57
+ lblv.style.cursor = "pointer";
58
+ const lblvL = document.createElement("div");
59
+ lblvL.className = "tp-lblv_l";
60
+ lblvL.textContent = "fps";
61
+ lblv.appendChild(lblvL);
62
+ const lblvV = document.createElement("div");
63
+ lblvV.className = "tp-lblv_v";
64
+ lblv.appendChild(lblvV);
65
+ const fpsv = document.createElement("div");
66
+ fpsv.className = "tp-fpsv";
67
+ lblvV.appendChild(fpsv);
68
+ const fpsvG = document.createElement("div");
69
+ fpsvG.className = "tp-fpsv_g";
70
+ fpsv.appendChild(fpsvG);
71
+ const grlv = document.createElement("div");
72
+ grlv.className = "tp-grlv";
73
+ fpsvG.appendChild(grlv);
74
+ const canvas = document.createElement("canvas");
75
+ canvas.classList.add("tp-grlv_g");
76
+ canvas.style.cssText = `display:block;width:100%;height:calc(var(--cnt-usz, 20px) * ${rows})`;
77
+ canvas.width = 1;
78
+ canvas.height = 1;
79
+ grlv.appendChild(canvas);
80
+ const gfx = canvas.getContext("2d");
81
+ const fpsvL = document.createElement("div");
82
+ fpsvL.className = "tp-fpsv_l";
83
+ fpsvL.style.cssText = "background:rgba(0,2,28,0.65);padding:1px 4px;border-radius:2px;";
84
+ fpsv.appendChild(fpsvL);
85
+ const valueSpan = document.createElement("span");
86
+ valueSpan.className = "tp-fpsv_v";
87
+ valueSpan.textContent = "--";
88
+ fpsvL.appendChild(valueSpan);
89
+ const unitSpan = document.createElement("span");
90
+ unitSpan.className = "tp-fpsv_u";
91
+ unitSpan.textContent = "FPS";
92
+ fpsvL.appendChild(unitSpan);
93
+ const blade = parent.addBlade({ view: "separator" });
94
+ const bladeEl = blade.element;
95
+ bladeEl.innerHTML = "";
96
+ bladeEl.className = "tp-cntv";
97
+ bladeEl.style.cssText = "";
98
+ bladeEl.appendChild(lblv);
99
+ function applyMode() {
100
+ lblvL.textContent = mode;
101
+ unitSpan.textContent = MODE_UNITS[mode];
102
+ }
103
+ applyMode();
104
+ lblv.addEventListener("click", () => {
105
+ const state = client.state;
106
+ do {
107
+ modeIndex = (modeIndex + 1) % MODES.length;
108
+ mode = MODES[modeIndex];
109
+ } while (mode === "mem" && state.heapUsedMB === void 0 || mode === "gpu" && !state.gpuModeEnabled);
110
+ applyMode();
111
+ });
112
+ let cachedW = 0;
113
+ let cachedH = 0;
114
+ const resizeObserver = new ResizeObserver(([entry]) => {
115
+ if (!entry) return;
116
+ const r = entry.contentRect;
117
+ const dpr = window.devicePixelRatio || 1;
118
+ const w = Math.max(1, Math.round(r.width * dpr));
119
+ const h = Math.max(1, Math.round(r.height * dpr));
120
+ if (canvas.width !== w || canvas.height !== h) {
121
+ canvas.width = w;
122
+ canvas.height = h;
123
+ }
124
+ cachedW = w;
125
+ cachedH = h;
126
+ });
127
+ resizeObserver.observe(canvas);
128
+ const MODE_LABEL = { fps: "fps", ms: "ms", gpu: "gpu", mem: "mem" };
129
+ const MODE_VALUE_UNIT = { fps: "FPS", ms: "MS", gpu: "MS", mem: "MB" };
130
+ const MODE_RANGE_UNIT = { fps: "FPS", ms: "MS", gpu: "GPU", mem: "MB" };
131
+ const MODE_DECIMALS = { fps: 0, ms: 1, gpu: 1, mem: 0 };
132
+ const MODE_SCALE = { fps: 1, ms: 10, gpu: 10, mem: 1 };
133
+ const fmtCache = {
134
+ fps: { key: NaN, str: "" },
135
+ ms: { key: NaN, str: "" },
136
+ gpu: { key: NaN, str: "" },
137
+ mem: { key: NaN, str: "" }
138
+ };
139
+ function fmtCached(m, v) {
140
+ const key = Math.round(v * MODE_SCALE[m]);
141
+ const c = fmtCache[m];
142
+ if (c.key === key) return c.str;
143
+ c.key = key;
144
+ c.str = v.toFixed(MODE_DECIMALS[m]);
145
+ return c.str;
146
+ }
147
+ function seriesFor(m) {
148
+ const s = client.state.series;
149
+ switch (m) {
150
+ case "fps":
151
+ return s.fps;
152
+ case "ms":
153
+ return s.cpuMs;
154
+ case "gpu":
155
+ return s.gpuMs;
156
+ case "mem":
157
+ return s.heapUsedMB;
158
+ }
159
+ }
160
+ const prevLabel = { fps: 0, ms: 0, gpu: 0, mem: 0 };
161
+ const currLabel = { fps: 0, ms: 0, gpu: 0, mem: 0 };
162
+ const hasLabel = { fps: false, ms: false, gpu: false, mem: false };
163
+ let batchCount = 0;
164
+ let lastBatchAt = performance.now();
165
+ let prevRingWrite = 0;
166
+ const snap = (m, v) => {
167
+ if (v === void 0) return;
168
+ prevLabel[m] = hasLabel[m] ? currLabel[m] : v;
169
+ currLabel[m] = v;
170
+ hasLabel[m] = true;
171
+ };
172
+ const unsubscribe = client.addListener((s) => {
173
+ if (s.heapLimitMB !== void 0) graphMax.mem = s.heapLimitMB;
174
+ snap("fps", s.fps);
175
+ snap("ms", s.cpuMs);
176
+ snap("gpu", s.gpuMs);
177
+ snap("mem", s.heapUsedMB);
178
+ const fps = s.series.fps;
179
+ const size = fps.data.length;
180
+ const delta = (fps.write - prevRingWrite + size) % size;
181
+ if (delta > 0) {
182
+ batchCount = delta;
183
+ prevRingWrite = fps.write;
184
+ lastBatchAt = performance.now();
185
+ }
186
+ });
187
+ let rafId = 0;
188
+ let disposed = false;
189
+ function setText(node, next, cache) {
190
+ if (cache.v === next) return;
191
+ node.textContent = next;
192
+ cache.v = next;
193
+ }
194
+ const labelCache = { v: "" };
195
+ const valueCache = { v: "" };
196
+ const unitCache = { v: "" };
197
+ const titleCache = { v: "" };
198
+ function update() {
199
+ if (disposed) return;
200
+ if (cachedW === 0 || cachedH === 0 || gfx === null) return;
201
+ const now = performance.now();
202
+ const t = Math.min(1, Math.max(0, (now - lastBatchAt) / import_debug_protocol.STATS_BATCH_MS));
203
+ const ring = seriesFor(mode);
204
+ const size = ring.data.length;
205
+ const max = graphMax[mode] || 1;
206
+ const phase = batchCount * (1 - t);
207
+ const intPhase = Math.floor(phase);
208
+ const fracPhase = phase - intPhase;
209
+ const w = cachedW;
210
+ const h = cachedH;
211
+ gfx.clearRect(0, 0, w, h);
212
+ const color = MODE_COLORS[mode];
213
+ const samplePixelW = w / (BUFFER_SIZE - 1);
214
+ let mn = Infinity;
215
+ let mx = 0;
216
+ const baseRingIdx = ring.write - intPhase - BUFFER_SIZE + size * 2;
217
+ gfx.beginPath();
218
+ for (let i = -1; i < BUFFER_SIZE; i++) {
219
+ const ringIdx = (baseRingIdx + i) % size;
220
+ const v = ring.data[ringIdx];
221
+ if (i >= 0) {
222
+ if (v < mn) mn = v;
223
+ if (v > mx) mx = v;
224
+ }
225
+ const x = (i + fracPhase) * samplePixelW;
226
+ const yRaw = h - v / max * h * 0.85 - h * 0.05;
227
+ const y = yRaw < h * 0.05 ? h * 0.05 : yRaw;
228
+ if (i === -1) gfx.moveTo(x, y);
229
+ else gfx.lineTo(x, y);
230
+ }
231
+ gfx.lineTo(w, h);
232
+ gfx.lineTo(0, h);
233
+ gfx.closePath();
234
+ gfx.fillStyle = color;
235
+ gfx.globalAlpha = 0.33;
236
+ gfx.fill();
237
+ gfx.globalAlpha = 1;
238
+ gfx.beginPath();
239
+ for (let i = -1; i < BUFFER_SIZE; i++) {
240
+ const ringIdx = (baseRingIdx + i) % size;
241
+ const v = ring.data[ringIdx];
242
+ const x = (i + fracPhase) * samplePixelW;
243
+ const yRaw = h - v / max * h * 0.85 - h * 0.05;
244
+ const y = yRaw < h * 0.05 ? h * 0.05 : yRaw;
245
+ if (i === -1) gfx.moveTo(x, y);
246
+ else gfx.lineTo(x, y);
247
+ }
248
+ gfx.strokeStyle = color;
249
+ gfx.lineWidth = Math.max(1, window.devicePixelRatio || 1);
250
+ gfx.stroke();
251
+ if (hasLabel[mode]) {
252
+ sessionMin[mode] = Math.min(sessionMin[mode], mn);
253
+ sessionMax[mode] = Math.max(sessionMax[mode], mx);
254
+ }
255
+ const has = hasLabel[mode];
256
+ const lerped = prevLabel[mode] + (currLabel[mode] - prevLabel[mode]) * t;
257
+ const sMn = sessionMin[mode];
258
+ const sMx = sessionMax[mode];
259
+ setText(lblvL, MODE_LABEL[mode], labelCache);
260
+ setText(valueSpan, has ? fmtCached(mode, lerped) : "--", valueCache);
261
+ setText(unitSpan, MODE_VALUE_UNIT[mode], unitCache);
262
+ const nextTitle = sMn !== Infinity ? `${MODE_RANGE_UNIT[mode]} range: ${Math.round(sMn)}\u2013${Math.round(sMx)}` : "";
263
+ if (titleCache.v !== nextTitle) {
264
+ lblvL.title = nextTitle;
265
+ titleCache.v = nextTitle;
266
+ }
267
+ }
268
+ if (driver === "raf") {
269
+ const autoFrame = () => {
270
+ if (disposed) return;
271
+ rafId = requestAnimationFrame(autoFrame);
272
+ update();
273
+ };
274
+ rafId = requestAnimationFrame(autoFrame);
275
+ }
276
+ return {
277
+ element: bladeEl,
278
+ // In `'raf'` mode the internal loop already runs, so public `update()`
279
+ // is a no-op — we don't want to paint twice per frame when the host
280
+ // also (redundantly) drives it. In `'manual'` mode this is the only
281
+ // way the graph gets painted.
282
+ update: driver === "manual" ? update : () => {
283
+ },
284
+ dispose() {
285
+ disposed = true;
286
+ if (rafId !== 0) cancelAnimationFrame(rafId);
287
+ unsubscribe();
288
+ resizeObserver.disconnect();
289
+ blade.dispose();
290
+ }
291
+ };
292
+ }
293
+ // Annotate the CommonJS export names for ESM import in node:
294
+ 0 && (module.exports = {
295
+ addStatsGraph
296
+ });
297
+ //# sourceMappingURL=stats-graph.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stats-graph.ts"],"sourcesContent":["/**\n * Cycling stats graph — click to cycle FPS / MS / MEM (like stats.js).\n *\n * Replicates the exact DOM structure of Tweakpane's built-in fpsgraph blade.\n * Uses SVG polyline like GraphLogView (tp-grlv). Click anywhere to cycle modes.\n * Only the graph line is colorized per mode; text uses default Tweakpane colors.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\nimport { STATS_BATCH_MS } from 'three-flatland/debug-protocol'\n\nimport type { DevtoolsClient, DevtoolsSeries } from './devtools-client.js'\n\nexport interface StatsGraphHandle {\n /** The root element */\n readonly element: HTMLElement\n /**\n * Render a frame. Required when constructed with `driver: 'manual'`;\n * no-op for the `'raf'` driver (the internal rAF is doing it).\n */\n update(): void\n /** Stop and remove. */\n dispose(): void\n}\n\nexport interface AddStatsGraphOptions {\n rows?: number\n /**\n * `'raf'` (default) — the graph runs its own `requestAnimationFrame`\n * loop. `'manual'` — no rAF; caller drives via `handle.update()` from\n * their own frame tick (e.g. `renderer.setAnimationLoop`, R3F\n * `useFrame`).\n */\n driver?: 'raf' | 'manual'\n}\n\ntype Mode = 'fps' | 'ms' | 'gpu' | 'mem'\n\n// Cycle order: FPS → MS → GPU → MEM (gpu next to ms so you can A/B them).\nconst MODES: Mode[] = ['fps', 'ms', 'gpu', 'mem']\nconst BUFFER_SIZE = 80\n\n// Retro-themed line colors (only applied to the graph line)\nconst MODE_COLORS: Record<Mode, string> = {\n fps: '#47cca9', // retro-cyan\n ms: '#47cc6a', // green variant\n gpu: '#ffa347', // retro-amber\n mem: '#d94c87', // retro-pink\n}\n\n// Unit labels shown next to the live value overlay (bottom-right of the\n// graph). The left-side mode label distinguishes fps/ms/gpu/mem; this is\n// just the unit of the number itself, so `gpu` (also milliseconds) reads\n// `MS` here.\nconst MODE_UNITS: Record<Mode, string> = {\n fps: 'FPS',\n ms: 'MS',\n gpu: 'MS',\n mem: 'MB',\n}\n\n/**\n * Add a cycling stats graph to a Tweakpane parent.\n * Matches the visual design of the built-in fpsgraph blade.\n */\nexport function addStatsGraph(\n parent: Pane | FolderApi,\n client: DevtoolsClient,\n options: AddStatsGraphOptions = {},\n): StatsGraphHandle {\n const { rows = 2, driver = 'raf' } = options\n\n // State\n let mode: Mode = 'fps'\n let modeIndex = 0\n\n // Graph max scales\n // FPS: 120 (tops out at 120fps)\n // MS: 16.67 (one frame at 60fps — hitting the top means blown frame budget)\n // GPU: 16.67 (same frame budget as MS, just measured on the GPU side)\n // MEM: heap limit in MB — pulled from `state.heapLimitMB` once it\n // arrives; default 256 until then.\n const graphMax: Record<Mode, number> = {\n fps: 120,\n ms: 16.67,\n gpu: 16.67,\n mem: 256,\n }\n\n // Session min/max tracking (like stats.js displays \"60 FPS (55-62)\")\n const sessionMin: Record<Mode, number> = { fps: Infinity, ms: Infinity, gpu: Infinity, mem: Infinity }\n const sessionMax: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n\n // ── Build DOM matching Tweakpane's label + graph structure ──\n\n // tp-lblv: label view (left label + right value)\n const lblv = document.createElement('div')\n lblv.className = 'tp-lblv'\n lblv.style.cursor = 'pointer'\n\n const lblvL = document.createElement('div')\n lblvL.className = 'tp-lblv_l'\n lblvL.textContent = 'fps'\n lblv.appendChild(lblvL)\n\n // Value area (right side — full width when no label)\n const lblvV = document.createElement('div')\n lblvV.className = 'tp-lblv_v'\n lblv.appendChild(lblvV)\n\n // tp-fpsv: FPS view container (positions the overlay label)\n const fpsv = document.createElement('div')\n fpsv.className = 'tp-fpsv'\n lblvV.appendChild(fpsv)\n\n // tp-fpsv_g: graph area\n const fpsvG = document.createElement('div')\n fpsvG.className = 'tp-fpsv_g'\n fpsv.appendChild(fpsvG)\n\n // tp-grlv: graph log view. We render into a 2D canvas instead of an\n // SVG polyline because per-frame `setAttribute('points', ...)` was\n // (a) allocating long template-literal strings every rAF and (b)\n // forcing the browser to re-allocate CSS-selector strings to\n // re-evaluate styles for every ancestor in the `.tp-cntv .tp-v-fst …`\n // chain — a measurable string-GC cost in profiles. Canvas drawing is\n // pure path commands, no DOM mutation.\n const grlv = document.createElement('div')\n grlv.className = 'tp-grlv'\n fpsvG.appendChild(grlv)\n\n const canvas = document.createElement('canvas')\n canvas.classList.add('tp-grlv_g')\n canvas.style.cssText = `display:block;width:100%;height:calc(var(--cnt-usz, 20px) * ${rows})`\n canvas.width = 1\n canvas.height = 1\n grlv.appendChild(canvas)\n const gfx = canvas.getContext('2d')\n\n // tp-fpsv_l: value label overlay (bottom-right) with bg for readability\n const fpsvL = document.createElement('div')\n fpsvL.className = 'tp-fpsv_l'\n fpsvL.style.cssText = 'background:rgba(0,2,28,0.65);padding:1px 4px;border-radius:2px;'\n fpsv.appendChild(fpsvL)\n\n const valueSpan = document.createElement('span')\n valueSpan.className = 'tp-fpsv_v'\n valueSpan.textContent = '--'\n fpsvL.appendChild(valueSpan)\n\n const unitSpan = document.createElement('span')\n unitSpan.className = 'tp-fpsv_u'\n unitSpan.textContent = 'FPS'\n fpsvL.appendChild(unitSpan)\n\n // ── Inject into Tweakpane via a real blade slot ──\n // Use addBlade to get a proper slot in the blade rack (maintains ordering).\n // Then replace the blade element's inner content with our custom graph.\n\n const blade = parent.addBlade({ view: 'separator' })\n const bladeEl = blade.element\n\n // Replace separator content with our graph layout\n bladeEl.innerHTML = ''\n bladeEl.className = 'tp-cntv'\n bladeEl.style.cssText = ''\n bladeEl.appendChild(lblv)\n\n // ── Mode cycling ──\n\n function applyMode() {\n lblvL.textContent = mode\n unitSpan.textContent = MODE_UNITS[mode]\n }\n applyMode()\n\n lblv.addEventListener('click', () => {\n // Only cycle to modes the producer is actually streaming values for\n // (gpuMs for gpu, heapUsedMB for mem). Client state is the source of\n // truth — no separate enabled flags.\n const state = client.state\n do {\n modeIndex = (modeIndex + 1) % MODES.length\n mode = MODES[modeIndex]!\n } while (\n (mode === 'mem' && state.heapUsedMB === undefined) ||\n (mode === 'gpu' && !state.gpuModeEnabled)\n )\n applyMode()\n })\n\n // ── Graph rendering ──\n\n // Sync canvas backing to CSS box × DPR so 1 source pixel = 1 device\n // pixel; avoids per-frame `getBoundingClientRect` which Safari pays\n // dearly for, and keeps the polyline crisp on HiDPI.\n let cachedW = 0\n let cachedH = 0\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return\n const r = entry.contentRect\n const dpr = window.devicePixelRatio || 1\n const w = Math.max(1, Math.round(r.width * dpr))\n const h = Math.max(1, Math.round(r.height * dpr))\n if (canvas.width !== w || canvas.height !== h) {\n canvas.width = w\n canvas.height = h\n }\n cachedW = w\n cachedH = h\n })\n resizeObserver.observe(canvas)\n\n const MODE_LABEL: Record<Mode, string> = { fps: 'fps', ms: 'ms', gpu: 'gpu', mem: 'mem' }\n const MODE_VALUE_UNIT: Record<Mode, string> = { fps: 'FPS', ms: 'MS', gpu: 'MS', mem: 'MB' }\n const MODE_RANGE_UNIT: Record<Mode, string> = { fps: 'FPS', ms: 'MS', gpu: 'GPU', mem: 'MB' }\n const MODE_DECIMALS: Record<Mode, number> = { fps: 0, ms: 1, gpu: 1, mem: 0 }\n const MODE_SCALE: Record<Mode, number> = { fps: 1, ms: 10, gpu: 10, mem: 1 }\n\n // Rounded-integer-keyed cache for the `toFixed` result per mode.\n // `toFixed` allocates a fresh string every call; with this cache,\n // we only allocate when the rounded display value actually changes\n // (e.g. fps stays at 60 for many rAF ticks while the lerped value\n // wiggles in the high 59s — single string reused for the whole run).\n const fmtCache: Record<Mode, { key: number; str: string }> = {\n fps: { key: NaN, str: '' },\n ms: { key: NaN, str: '' },\n gpu: { key: NaN, str: '' },\n mem: { key: NaN, str: '' },\n }\n function fmtCached(m: Mode, v: number): string {\n const key = Math.round(v * MODE_SCALE[m])\n const c = fmtCache[m]\n if (c.key === key) return c.str\n c.key = key\n c.str = v.toFixed(MODE_DECIMALS[m])\n return c.str\n }\n\n function seriesFor(m: Mode): DevtoolsSeries {\n const s = client.state.series\n switch (m) {\n case 'fps': return s.fps\n case 'ms': return s.cpuMs\n case 'gpu': return s.gpuMs\n case 'mem': return s.heapUsedMB\n }\n }\n\n // ── Arrival snapshots (written in the listener, read in RAF) ──────────\n // The listener only snapshots values and resets the interpolation\n // clock. RAF does all rendering, so the display stays smooth between\n // the 4 Hz batches.\n const prevLabel: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n const currLabel: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n const hasLabel: Record<Mode, boolean> = { fps: false, ms: false, gpu: false, mem: false }\n let batchCount = 0\n let lastBatchAt = performance.now()\n let prevRingWrite = 0\n\n const snap = (m: Mode, v: number | undefined): void => {\n if (v === undefined) return\n prevLabel[m] = hasLabel[m] ? currLabel[m] : v\n currLabel[m] = v\n hasLabel[m] = true\n }\n\n const unsubscribe = client.addListener((s) => {\n if (s.heapLimitMB !== undefined) graphMax.mem = s.heapLimitMB\n snap('fps', s.fps)\n snap('ms', s.cpuMs)\n snap('gpu', s.gpuMs)\n snap('mem', s.heapUsedMB)\n // How many new samples came in this batch? All series write in\n // lockstep, so we can just watch `fps`.\n const fps = s.series.fps\n const size = fps.data.length\n const delta = (fps.write - prevRingWrite + size) % size\n if (delta > 0) {\n batchCount = delta\n prevRingWrite = fps.write\n lastBatchAt = performance.now()\n }\n })\n\n // ── Render ────────────────────────────────────────────────────────────\n // Canvas path-based draw — no DOM attribute mutation per frame, no\n // string allocations beyond the unavoidable `toFixed` for the text\n // label (and even those are deduped: textContent is only re-assigned\n // when the rendered string actually changes).\n\n let rafId = 0\n let disposed = false\n\n function setText(node: HTMLElement, next: string, cache: { v: string }): void {\n if (cache.v === next) return\n node.textContent = next\n cache.v = next\n }\n // Boxed cache holders keep `setText` allocation-free past creation.\n const labelCache = { v: '' }\n const valueCache = { v: '' }\n const unitCache = { v: '' }\n const titleCache = { v: '' }\n\n function update(): void {\n if (disposed) return\n if (cachedW === 0 || cachedH === 0 || gfx === null) return\n\n const now = performance.now()\n const t = Math.min(1, Math.max(0, (now - lastBatchAt) / STATS_BATCH_MS))\n\n // Slide the display window through the ring so newest samples fade\n // in at 60 Hz between the producer's 4 Hz batches.\n //\n // **Don't blend sample VALUES across the slide.** Earlier the\n // sample at each pixel was `a*(1-frac) + b*frac` for a\n // continuously-changing `frac` — that means a single peak at\n // ring slot K shows different *heights* at different sub-batch\n // times (full height only when frac=0; lower whenever it's\n // blended with a neighbor). Visible as peaks \"changing shape as\n // they scroll.\" Sample each ring slot at its INTEGER index — the\n // height never moves — and shift the entire polyline's X by the\n // fractional sub-sample offset. The line slides smoothly across\n // the canvas; each sample's height is the same in every frame\n // that contains it.\n const ring = seriesFor(mode)\n const size = ring.data.length\n const max = graphMax[mode] || 1\n const phase = batchCount * (1 - t) // continuous 0..batchCount\n const intPhase = Math.floor(phase) // integer slide steps remaining\n const fracPhase = phase - intPhase // 0..1 sub-step (sub-pixel scroll)\n\n const w = cachedW\n const h = cachedH\n gfx.clearRect(0, 0, w, h)\n\n const color = MODE_COLORS[mode]\n const samplePixelW = w / (BUFFER_SIZE - 1)\n\n // Draw BUFFER_SIZE + 1 samples — one extra at i=-1 so the leftmost\n // visible pixel always has a polyline endpoint, no matter where\n // fracPhase sits between integer steps. Without this extra, the\n // line would have a visible gap at the canvas's left edge during\n // sub-pixel slide.\n //\n // Two passes (fill polygon, then crisp stroke) over the same\n // points; computing each point in a tight inner loop keeps allocs\n // at zero. The values cache `mn`/`mx` are session-min/max trackers,\n // populated from the ring (not the rendered polyline) — they\n // bypass the integer-index loop and read raw ring values.\n let mn = Infinity\n let mx = 0\n // Walk the same range we render to compute mn/mx — no separate\n // pass. Adds an `if` per sample but skips a second loop.\n const baseRingIdx = ring.write - intPhase - BUFFER_SIZE + size * 2\n\n gfx.beginPath()\n for (let i = -1; i < BUFFER_SIZE; i++) {\n const ringIdx = (baseRingIdx + i) % size\n const v = ring.data[ringIdx]!\n if (i >= 0) {\n if (v < mn) mn = v\n if (v > mx) mx = v\n }\n const x = (i + fracPhase) * samplePixelW\n // Clamp y to the canvas top so over-budget samples (v > graphMax)\n // saturate at the top edge instead of drawing above the canvas.\n const yRaw = h - (v / max) * h * 0.85 - h * 0.05\n const y = yRaw < h * 0.05 ? h * 0.05 : yRaw\n if (i === -1) gfx.moveTo(x, y)\n else gfx.lineTo(x, y)\n }\n\n // Translucent fill under the line. Close to bottom-right then\n // bottom-left of the canvas — the polyline's leftmost x may be\n // slightly negative during sub-pixel slide, but the canvas crops\n // off-edge pixels for free, so the polygon closes cleanly.\n gfx.lineTo(w, h)\n gfx.lineTo(0, h)\n gfx.closePath()\n gfx.fillStyle = color\n gfx.globalAlpha = 0.33\n gfx.fill()\n gfx.globalAlpha = 1\n\n // Re-stroke the line over the fill so the top edge is sharp.\n gfx.beginPath()\n for (let i = -1; i < BUFFER_SIZE; i++) {\n const ringIdx = (baseRingIdx + i) % size\n const v = ring.data[ringIdx]!\n const x = (i + fracPhase) * samplePixelW\n const yRaw = h - (v / max) * h * 0.85 - h * 0.05\n const y = yRaw < h * 0.05 ? h * 0.05 : yRaw\n if (i === -1) gfx.moveTo(x, y)\n else gfx.lineTo(x, y)\n }\n gfx.strokeStyle = color\n gfx.lineWidth = Math.max(1, (window.devicePixelRatio || 1))\n gfx.stroke()\n\n if (hasLabel[mode]) {\n sessionMin[mode] = Math.min(sessionMin[mode], mn)\n sessionMax[mode] = Math.max(sessionMax[mode], mx)\n }\n\n // Text — lerp the label's number toward the latest batch mean.\n // `toFixed` allocates a new string each call (~one per rAF) but\n // we dedupe assignment to avoid setting `textContent` redundantly,\n // which would trigger DOM mutation work even when the string is\n // identical.\n const has = hasLabel[mode]\n const lerped = prevLabel[mode] + (currLabel[mode] - prevLabel[mode]) * t\n const sMn = sessionMin[mode]\n const sMx = sessionMax[mode]\n setText(lblvL, MODE_LABEL[mode], labelCache)\n setText(valueSpan, has ? fmtCached(mode, lerped) : '--', valueCache)\n setText(unitSpan, MODE_VALUE_UNIT[mode], unitCache)\n const nextTitle = sMn !== Infinity ? `${MODE_RANGE_UNIT[mode]} range: ${Math.round(sMn)}–${Math.round(sMx)}` : ''\n if (titleCache.v !== nextTitle) {\n lblvL.title = nextTitle\n titleCache.v = nextTitle\n }\n }\n\n if (driver === 'raf') {\n const autoFrame = (): void => {\n if (disposed) return\n rafId = requestAnimationFrame(autoFrame)\n update()\n }\n rafId = requestAnimationFrame(autoFrame)\n }\n\n return {\n element: bladeEl,\n // In `'raf'` mode the internal loop already runs, so public `update()`\n // is a no-op — we don't want to paint twice per frame when the host\n // also (redundantly) drives it. In `'manual'` mode this is the only\n // way the graph gets painted.\n update: driver === 'manual' ? update : () => {},\n dispose() {\n disposed = true\n if (rafId !== 0) cancelAnimationFrame(rafId)\n unsubscribe()\n resizeObserver.disconnect()\n blade.dispose()\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,4BAA+B;AA8B/B,MAAM,QAAgB,CAAC,OAAO,MAAM,OAAO,KAAK;AAChD,MAAM,cAAc;AAGpB,MAAM,cAAoC;AAAA,EACxC,KAAK;AAAA;AAAA,EACL,IAAI;AAAA;AAAA,EACJ,KAAK;AAAA;AAAA,EACL,KAAK;AAAA;AACP;AAMA,MAAM,aAAmC;AAAA,EACvC,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,KAAK;AACP;AAMO,SAAS,cACd,QACA,QACA,UAAgC,CAAC,GACf;AAClB,QAAM,EAAE,OAAO,GAAG,SAAS,MAAM,IAAI;AAGrC,MAAI,OAAa;AACjB,MAAI,YAAY;AAQhB,QAAM,WAAiC;AAAA,IACrC,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAGA,QAAM,aAAmC,EAAE,KAAK,UAAU,IAAI,UAAU,KAAK,UAAU,KAAK,SAAS;AACrG,QAAM,aAAmC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,EAAE;AAKzE,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,YAAY;AACjB,OAAK,MAAM,SAAS;AAEpB,QAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,QAAM,YAAY;AAClB,QAAM,cAAc;AACpB,OAAK,YAAY,KAAK;AAGtB,QAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,QAAM,YAAY;AAClB,OAAK,YAAY,KAAK;AAGtB,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,YAAY;AACjB,QAAM,YAAY,IAAI;AAGtB,QAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,QAAM,YAAY;AAClB,OAAK,YAAY,KAAK;AAStB,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,YAAY;AACjB,QAAM,YAAY,IAAI;AAEtB,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,UAAU,IAAI,WAAW;AAChC,SAAO,MAAM,UAAU,+DAA+D,IAAI;AAC1F,SAAO,QAAQ;AACf,SAAO,SAAS;AAChB,OAAK,YAAY,MAAM;AACvB,QAAM,MAAM,OAAO,WAAW,IAAI;AAGlC,QAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,QAAM,YAAY;AAClB,QAAM,MAAM,UAAU;AACtB,OAAK,YAAY,KAAK;AAEtB,QAAM,YAAY,SAAS,cAAc,MAAM;AAC/C,YAAU,YAAY;AACtB,YAAU,cAAc;AACxB,QAAM,YAAY,SAAS;AAE3B,QAAM,WAAW,SAAS,cAAc,MAAM;AAC9C,WAAS,YAAY;AACrB,WAAS,cAAc;AACvB,QAAM,YAAY,QAAQ;AAM1B,QAAM,QAAQ,OAAO,SAAS,EAAE,MAAM,YAAY,CAAC;AACnD,QAAM,UAAU,MAAM;AAGtB,UAAQ,YAAY;AACpB,UAAQ,YAAY;AACpB,UAAQ,MAAM,UAAU;AACxB,UAAQ,YAAY,IAAI;AAIxB,WAAS,YAAY;AACnB,UAAM,cAAc;AACpB,aAAS,cAAc,WAAW,IAAI;AAAA,EACxC;AACA,YAAU;AAEV,OAAK,iBAAiB,SAAS,MAAM;AAInC,UAAM,QAAQ,OAAO;AACrB,OAAG;AACD,mBAAa,YAAY,KAAK,MAAM;AACpC,aAAO,MAAM,SAAS;AAAA,IACxB,SACG,SAAS,SAAS,MAAM,eAAe,UACvC,SAAS,SAAS,CAAC,MAAM;AAE5B,cAAU;AAAA,EACZ,CAAC;AAOD,MAAI,UAAU;AACd,MAAI,UAAU;AACd,QAAM,iBAAiB,IAAI,eAAe,CAAC,CAAC,KAAK,MAAM;AACrD,QAAI,CAAC,MAAO;AACZ,UAAM,IAAI,MAAM;AAChB,UAAM,MAAM,OAAO,oBAAoB;AACvC,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,EAAE,QAAQ,GAAG,CAAC;AAC/C,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,EAAE,SAAS,GAAG,CAAC;AAChD,QAAI,OAAO,UAAU,KAAK,OAAO,WAAW,GAAG;AAC7C,aAAO,QAAQ;AACf,aAAO,SAAS;AAAA,IAClB;AACA,cAAU;AACV,cAAU;AAAA,EACZ,CAAC;AACD,iBAAe,QAAQ,MAAM;AAE7B,QAAM,aAAmC,EAAE,KAAK,OAAO,IAAI,MAAM,KAAK,OAAO,KAAK,MAAM;AACxF,QAAM,kBAAwC,EAAE,KAAK,OAAO,IAAI,MAAM,KAAK,MAAM,KAAK,KAAK;AAC3F,QAAM,kBAAwC,EAAE,KAAK,OAAO,IAAI,MAAM,KAAK,OAAO,KAAK,KAAK;AAC5F,QAAM,gBAAsC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,EAAE;AAC5E,QAAM,aAAmC,EAAE,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,KAAK,EAAE;AAO3E,QAAM,WAAuD;AAAA,IAC3D,KAAK,EAAE,KAAK,KAAK,KAAK,GAAG;AAAA,IACzB,IAAI,EAAE,KAAK,KAAK,KAAK,GAAG;AAAA,IACxB,KAAK,EAAE,KAAK,KAAK,KAAK,GAAG;AAAA,IACzB,KAAK,EAAE,KAAK,KAAK,KAAK,GAAG;AAAA,EAC3B;AACA,WAAS,UAAU,GAAS,GAAmB;AAC7C,UAAM,MAAM,KAAK,MAAM,IAAI,WAAW,CAAC,CAAC;AACxC,UAAM,IAAI,SAAS,CAAC;AACpB,QAAI,EAAE,QAAQ,IAAK,QAAO,EAAE;AAC5B,MAAE,MAAM;AACR,MAAE,MAAM,EAAE,QAAQ,cAAc,CAAC,CAAC;AAClC,WAAO,EAAE;AAAA,EACX;AAEA,WAAS,UAAU,GAAyB;AAC1C,UAAM,IAAI,OAAO,MAAM;AACvB,YAAQ,GAAG;AAAA,MACT,KAAK;AAAO,eAAO,EAAE;AAAA,MACrB,KAAK;AAAM,eAAO,EAAE;AAAA,MACpB,KAAK;AAAO,eAAO,EAAE;AAAA,MACrB,KAAK;AAAO,eAAO,EAAE;AAAA,IACvB;AAAA,EACF;AAMA,QAAM,YAAkC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,EAAE;AACxE,QAAM,YAAkC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,EAAE;AACxE,QAAM,WAAkC,EAAE,KAAK,OAAO,IAAI,OAAO,KAAK,OAAO,KAAK,MAAM;AACxF,MAAI,aAAa;AACjB,MAAI,cAAc,YAAY,IAAI;AAClC,MAAI,gBAAgB;AAEpB,QAAM,OAAO,CAAC,GAAS,MAAgC;AACrD,QAAI,MAAM,OAAW;AACrB,cAAU,CAAC,IAAI,SAAS,CAAC,IAAI,UAAU,CAAC,IAAI;AAC5C,cAAU,CAAC,IAAI;AACf,aAAS,CAAC,IAAI;AAAA,EAChB;AAEA,QAAM,cAAc,OAAO,YAAY,CAAC,MAAM;AAC5C,QAAI,EAAE,gBAAgB,OAAW,UAAS,MAAM,EAAE;AAClD,SAAK,OAAO,EAAE,GAAG;AACjB,SAAK,MAAM,EAAE,KAAK;AAClB,SAAK,OAAO,EAAE,KAAK;AACnB,SAAK,OAAO,EAAE,UAAU;AAGxB,UAAM,MAAM,EAAE,OAAO;AACrB,UAAM,OAAO,IAAI,KAAK;AACtB,UAAM,SAAS,IAAI,QAAQ,gBAAgB,QAAQ;AACnD,QAAI,QAAQ,GAAG;AACb,mBAAa;AACb,sBAAgB,IAAI;AACpB,oBAAc,YAAY,IAAI;AAAA,IAChC;AAAA,EACF,CAAC;AAQD,MAAI,QAAQ;AACZ,MAAI,WAAW;AAEf,WAAS,QAAQ,MAAmB,MAAc,OAA4B;AAC5E,QAAI,MAAM,MAAM,KAAM;AACtB,SAAK,cAAc;AACnB,UAAM,IAAI;AAAA,EACZ;AAEA,QAAM,aAAa,EAAE,GAAG,GAAG;AAC3B,QAAM,aAAa,EAAE,GAAG,GAAG;AAC3B,QAAM,YAAY,EAAE,GAAG,GAAG;AAC1B,QAAM,aAAa,EAAE,GAAG,GAAG;AAE3B,WAAS,SAAe;AACtB,QAAI,SAAU;AACd,QAAI,YAAY,KAAK,YAAY,KAAK,QAAQ,KAAM;AAEpD,UAAM,MAAM,YAAY,IAAI;AAC5B,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,MAAM,eAAe,oCAAc,CAAC;AAgBvE,UAAM,OAAO,UAAU,IAAI;AAC3B,UAAM,OAAO,KAAK,KAAK;AACvB,UAAM,MAAM,SAAS,IAAI,KAAK;AAC9B,UAAM,QAAQ,cAAc,IAAI;AAChC,UAAM,WAAW,KAAK,MAAM,KAAK;AACjC,UAAM,YAAY,QAAQ;AAE1B,UAAM,IAAI;AACV,UAAM,IAAI;AACV,QAAI,UAAU,GAAG,GAAG,GAAG,CAAC;AAExB,UAAM,QAAQ,YAAY,IAAI;AAC9B,UAAM,eAAe,KAAK,cAAc;AAaxC,QAAI,KAAK;AACT,QAAI,KAAK;AAGT,UAAM,cAAc,KAAK,QAAQ,WAAW,cAAc,OAAO;AAEjE,QAAI,UAAU;AACd,aAAS,IAAI,IAAI,IAAI,aAAa,KAAK;AACrC,YAAM,WAAW,cAAc,KAAK;AACpC,YAAM,IAAI,KAAK,KAAK,OAAO;AAC3B,UAAI,KAAK,GAAG;AACV,YAAI,IAAI,GAAI,MAAK;AACjB,YAAI,IAAI,GAAI,MAAK;AAAA,MACnB;AACA,YAAM,KAAK,IAAI,aAAa;AAG5B,YAAM,OAAO,IAAK,IAAI,MAAO,IAAI,OAAO,IAAI;AAC5C,YAAM,IAAI,OAAO,IAAI,OAAO,IAAI,OAAO;AACvC,UAAI,MAAM,GAAI,KAAI,OAAO,GAAG,CAAC;AAAA,UACxB,KAAI,OAAO,GAAG,CAAC;AAAA,IACtB;AAMA,QAAI,OAAO,GAAG,CAAC;AACf,QAAI,OAAO,GAAG,CAAC;AACf,QAAI,UAAU;AACd,QAAI,YAAY;AAChB,QAAI,cAAc;AAClB,QAAI,KAAK;AACT,QAAI,cAAc;AAGlB,QAAI,UAAU;AACd,aAAS,IAAI,IAAI,IAAI,aAAa,KAAK;AACrC,YAAM,WAAW,cAAc,KAAK;AACpC,YAAM,IAAI,KAAK,KAAK,OAAO;AAC3B,YAAM,KAAK,IAAI,aAAa;AAC5B,YAAM,OAAO,IAAK,IAAI,MAAO,IAAI,OAAO,IAAI;AAC5C,YAAM,IAAI,OAAO,IAAI,OAAO,IAAI,OAAO;AACvC,UAAI,MAAM,GAAI,KAAI,OAAO,GAAG,CAAC;AAAA,UACxB,KAAI,OAAO,GAAG,CAAC;AAAA,IACtB;AACA,QAAI,cAAc;AAClB,QAAI,YAAY,KAAK,IAAI,GAAI,OAAO,oBAAoB,CAAE;AAC1D,QAAI,OAAO;AAEX,QAAI,SAAS,IAAI,GAAG;AAClB,iBAAW,IAAI,IAAI,KAAK,IAAI,WAAW,IAAI,GAAG,EAAE;AAChD,iBAAW,IAAI,IAAI,KAAK,IAAI,WAAW,IAAI,GAAG,EAAE;AAAA,IAClD;AAOA,UAAM,MAAM,SAAS,IAAI;AACzB,UAAM,SAAS,UAAU,IAAI,KAAK,UAAU,IAAI,IAAI,UAAU,IAAI,KAAK;AACvE,UAAM,MAAM,WAAW,IAAI;AAC3B,UAAM,MAAM,WAAW,IAAI;AAC3B,YAAQ,OAAO,WAAW,IAAI,GAAG,UAAU;AAC3C,YAAQ,WAAW,MAAM,UAAU,MAAM,MAAM,IAAI,MAAM,UAAU;AACnE,YAAQ,UAAU,gBAAgB,IAAI,GAAG,SAAS;AAClD,UAAM,YAAY,QAAQ,WAAW,GAAG,gBAAgB,IAAI,CAAC,WAAW,KAAK,MAAM,GAAG,CAAC,SAAI,KAAK,MAAM,GAAG,CAAC,KAAK;AAC/G,QAAI,WAAW,MAAM,WAAW;AAC9B,YAAM,QAAQ;AACd,iBAAW,IAAI;AAAA,IACjB;AAAA,EACF;AAEA,MAAI,WAAW,OAAO;AACpB,UAAM,YAAY,MAAY;AAC5B,UAAI,SAAU;AACd,cAAQ,sBAAsB,SAAS;AACvC,aAAO;AAAA,IACT;AACA,YAAQ,sBAAsB,SAAS;AAAA,EACzC;AAEA,SAAO;AAAA,IACL,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,IAKT,QAAQ,WAAW,WAAW,SAAS,MAAM;AAAA,IAAC;AAAA,IAC9C,UAAU;AACR,iBAAW;AACX,UAAI,UAAU,EAAG,sBAAqB,KAAK;AAC3C,kBAAY;AACZ,qBAAe,WAAW;AAC1B,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,40 @@
1
+ import { Pane, FolderApi } from 'tweakpane';
2
+ import { DevtoolsClient } from './devtools-client.cjs';
3
+ import 'three-flatland/debug-protocol';
4
+
5
+ /**
6
+ * Cycling stats graph — click to cycle FPS / MS / MEM (like stats.js).
7
+ *
8
+ * Replicates the exact DOM structure of Tweakpane's built-in fpsgraph blade.
9
+ * Uses SVG polyline like GraphLogView (tp-grlv). Click anywhere to cycle modes.
10
+ * Only the graph line is colorized per mode; text uses default Tweakpane colors.
11
+ */
12
+
13
+ interface StatsGraphHandle {
14
+ /** The root element */
15
+ readonly element: HTMLElement;
16
+ /**
17
+ * Render a frame. Required when constructed with `driver: 'manual'`;
18
+ * no-op for the `'raf'` driver (the internal rAF is doing it).
19
+ */
20
+ update(): void;
21
+ /** Stop and remove. */
22
+ dispose(): void;
23
+ }
24
+ interface AddStatsGraphOptions {
25
+ rows?: number;
26
+ /**
27
+ * `'raf'` (default) — the graph runs its own `requestAnimationFrame`
28
+ * loop. `'manual'` — no rAF; caller drives via `handle.update()` from
29
+ * their own frame tick (e.g. `renderer.setAnimationLoop`, R3F
30
+ * `useFrame`).
31
+ */
32
+ driver?: 'raf' | 'manual';
33
+ }
34
+ /**
35
+ * Add a cycling stats graph to a Tweakpane parent.
36
+ * Matches the visual design of the built-in fpsgraph blade.
37
+ */
38
+ declare function addStatsGraph(parent: Pane | FolderApi, client: DevtoolsClient, options?: AddStatsGraphOptions): StatsGraphHandle;
39
+
40
+ export { type AddStatsGraphOptions, type StatsGraphHandle, addStatsGraph };
@@ -0,0 +1,40 @@
1
+ import { Pane, FolderApi } from 'tweakpane';
2
+ import { DevtoolsClient } from './devtools-client.js';
3
+ import 'three-flatland/debug-protocol';
4
+
5
+ /**
6
+ * Cycling stats graph — click to cycle FPS / MS / MEM (like stats.js).
7
+ *
8
+ * Replicates the exact DOM structure of Tweakpane's built-in fpsgraph blade.
9
+ * Uses SVG polyline like GraphLogView (tp-grlv). Click anywhere to cycle modes.
10
+ * Only the graph line is colorized per mode; text uses default Tweakpane colors.
11
+ */
12
+
13
+ interface StatsGraphHandle {
14
+ /** The root element */
15
+ readonly element: HTMLElement;
16
+ /**
17
+ * Render a frame. Required when constructed with `driver: 'manual'`;
18
+ * no-op for the `'raf'` driver (the internal rAF is doing it).
19
+ */
20
+ update(): void;
21
+ /** Stop and remove. */
22
+ dispose(): void;
23
+ }
24
+ interface AddStatsGraphOptions {
25
+ rows?: number;
26
+ /**
27
+ * `'raf'` (default) — the graph runs its own `requestAnimationFrame`
28
+ * loop. `'manual'` — no rAF; caller drives via `handle.update()` from
29
+ * their own frame tick (e.g. `renderer.setAnimationLoop`, R3F
30
+ * `useFrame`).
31
+ */
32
+ driver?: 'raf' | 'manual';
33
+ }
34
+ /**
35
+ * Add a cycling stats graph to a Tweakpane parent.
36
+ * Matches the visual design of the built-in fpsgraph blade.
37
+ */
38
+ declare function addStatsGraph(parent: Pane | FolderApi, client: DevtoolsClient, options?: AddStatsGraphOptions): StatsGraphHandle;
39
+
40
+ export { type AddStatsGraphOptions, type StatsGraphHandle, addStatsGraph };