@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,18 @@
1
+ /** @jsxImportSource preact */
2
+ /**
3
+ * Devtools dashboard — standalone full-page inspector.
4
+ *
5
+ * Mounted by the Vite plugin at `/__three-flatland__/devtools`. Shares the
6
+ * same `BroadcastChannel` discovery as the Tweakpane client, so any producer
7
+ * the app creates is visible here automatically. No dependency on Tweakpane.
8
+ *
9
+ * Rendered with vendored Preact (see `./vendor/`). No runtime dep leaks into
10
+ * the published package — the plugin only serves this entry when the user
11
+ * opts in.
12
+ */
13
+ import { render } from 'preact'
14
+ import { App } from './app'
15
+
16
+ const root = document.getElementById('root')
17
+ if (root === null) throw new Error('[devtools-dashboard] #root missing')
18
+ render(<App />, root)
@@ -0,0 +1,282 @@
1
+ /** @jsxImportSource preact */
2
+ /**
3
+ * Batch inspector — renders the two arrays in
4
+ * `DevtoolsState.batches`:
5
+ *
6
+ * 1. **Active batches** (first) — the current contents of every batch
7
+ * source (ECS `BatchRegistry` + engine-owned `InstancedMesh`es like
8
+ * tilemap chunks) grouped by `(kind, runKey)`. Each run header
9
+ * shows a kind badge + material label + total instance count; each
10
+ * row shows the batch index or label and its live instance count.
11
+ *
12
+ * 2. **Passes** — a flat, indented list of render passes that fired
13
+ * this frame. Each parent pass (e.g. `frame`, `sdf`) has a caret
14
+ * that collapses its subtree. Collapse state is keyed by label
15
+ * (not index) so it survives frame-to-frame churn. Totals row at
16
+ * the bottom sums root-level entries.
17
+ *
18
+ * Sections themselves are not collapsible; the panel is always two
19
+ * tables. The whole panel is a single scroll container — inner lists
20
+ * don't create their own scrollbars.
21
+ */
22
+ import { useMemo, useState } from 'preact/hooks'
23
+ import { useDevtoolsState } from '../hooks.js'
24
+ import type { BatchPassSnapshot } from '../../devtools-client.js'
25
+
26
+ interface GroupedRun {
27
+ runKey: number
28
+ layer: number
29
+ materialId: number
30
+ materialName: string
31
+ kind: string
32
+ totalSprites: number
33
+ batches: Array<{ batchIdx: number; spriteCount: number; label: string }>
34
+ }
35
+
36
+ interface PassNode {
37
+ pass: BatchPassSnapshot
38
+ index: number
39
+ children: PassNode[]
40
+ }
41
+
42
+ export function BatchesPanel() {
43
+ const state = useDevtoolsState()
44
+ const batches = state.batches
45
+
46
+ /**
47
+ * Collapse state for pass subtrees. Keyed by pass label — labels
48
+ * (`frame`, `sdf`, `sdf.seed`, etc.) are stable string constants
49
+ * so the set survives per-frame rebuilds. Collapse state for leaf
50
+ * passes is irrelevant (no children to hide) so this set only ever
51
+ * contains parent labels.
52
+ */
53
+ const [collapsedPasses, setCollapsedPasses] = useState<Set<string>>(() => new Set())
54
+ const toggleCollapsed = (label: string) => {
55
+ setCollapsedPasses((prev) => {
56
+ const next = new Set(prev)
57
+ if (next.has(label)) next.delete(label)
58
+ else next.add(label)
59
+ return next
60
+ })
61
+ }
62
+
63
+ const passTotals = useMemo(() => {
64
+ let calls = 0
65
+ let tris = 0
66
+ let cpuMs = 0
67
+ for (const p of batches.passes) {
68
+ if (p.parent === -1) {
69
+ calls += p.calls
70
+ tris += p.triangles
71
+ cpuMs += p.cpuMs
72
+ }
73
+ }
74
+ return { calls, tris, cpuMs }
75
+ }, [batches.frame])
76
+
77
+ const runs = useMemo(() => {
78
+ const m = new Map<string, GroupedRun>()
79
+ for (const b of batches.batches) {
80
+ const groupKey = `${b.kind}:${b.runKey}`
81
+ let run = m.get(groupKey)
82
+ if (run === undefined) {
83
+ run = {
84
+ runKey: b.runKey,
85
+ layer: b.layer,
86
+ materialId: b.materialId,
87
+ materialName: b.materialName,
88
+ kind: b.kind,
89
+ totalSprites: 0,
90
+ batches: [],
91
+ }
92
+ m.set(groupKey, run)
93
+ }
94
+ run.totalSprites += b.spriteCount
95
+ run.batches.push({ batchIdx: b.batchIdx, spriteCount: b.spriteCount, label: b.label })
96
+ }
97
+ const arr = Array.from(m.values())
98
+ arr.sort((a, b) =>
99
+ a.kind.localeCompare(b.kind) || a.layer - b.layer || a.materialId - b.materialId,
100
+ )
101
+ return arr
102
+ }, [batches.frame])
103
+
104
+ const totalSprites = useMemo(() => {
105
+ let n = 0
106
+ for (const r of runs) n += r.totalSprites
107
+ return n
108
+ }, [runs])
109
+
110
+ /**
111
+ * Rebuild the pass tree from the flat array. Parents come before
112
+ * children in the producer's emission order (frame first, then its
113
+ * children, etc.) so one linear pass + index lookup suffices.
114
+ */
115
+ const passRoots = useMemo(() => {
116
+ const nodes: PassNode[] = batches.passes.map((p, i) => ({
117
+ pass: p,
118
+ index: i,
119
+ children: [],
120
+ }))
121
+ const roots: PassNode[] = []
122
+ for (const n of nodes) {
123
+ if (n.pass.parent === -1) roots.push(n)
124
+ else nodes[n.pass.parent]?.children.push(n)
125
+ }
126
+ return roots
127
+ }, [batches.frame])
128
+
129
+ return (
130
+ <section class="panel batches-panel">
131
+ <header class="panel-header batches-header">
132
+ <span>Batches</span>
133
+ <span class="batches-header-meta">
134
+ frame {batches.frame} · {passTotals.calls} draws · {runs.length} runs · {batches.batches.length} batches · {totalSprites} sprites
135
+ </span>
136
+ </header>
137
+
138
+ <div class="batches-scroll">
139
+ <div class="batches-section">
140
+ <div class="batches-section-title">
141
+ <span>Active batches</span>
142
+ <span class="batches-section-count">{batches.batches.length}</span>
143
+ </div>
144
+ <div class="batches-table-head batches-table-head--runs">
145
+ <span class="batches-col-label">run / batch</span>
146
+ <span class="batches-col-num">layer</span>
147
+ <span class="batches-col-num">sprites</span>
148
+ </div>
149
+ <ul class="batches-run-list">
150
+ {runs.length === 0 ? (
151
+ <li class="batches-empty">no active batches</li>
152
+ ) : (
153
+ runs.map((r) => (
154
+ <li key={`${r.kind}:${r.runKey}`} class="batches-run">
155
+ <div class="batches-run-head">
156
+ <span class="batches-col-label batches-run-label">
157
+ <span class={`batches-kind batches-kind--${r.kind}`}>{r.kind}</span>
158
+ <span class="batches-mat">{r.materialName}</span>
159
+ <span class="batches-run-sub">mat#{r.materialId}</span>
160
+ </span>
161
+ <span class="batches-col-num">{r.layer}</span>
162
+ <span class="batches-col-num">{r.totalSprites}</span>
163
+ </div>
164
+ {r.batches.length > 1 && (
165
+ <ul class="batches-run-children">
166
+ {r.batches.map((b) => (
167
+ <li key={b.batchIdx} class="batches-batch-row">
168
+ <span class="batches-col-label batches-batch-label">
169
+ {b.label.length > 0 ? b.label : `batch #${b.batchIdx}`}
170
+ </span>
171
+ <span class="batches-col-num">—</span>
172
+ <span class="batches-col-num">{b.spriteCount}</span>
173
+ </li>
174
+ ))}
175
+ </ul>
176
+ )}
177
+ </li>
178
+ ))
179
+ )}
180
+ </ul>
181
+ </div>
182
+
183
+ <div class="batches-section">
184
+ <div class="batches-section-title">
185
+ <span>Passes</span>
186
+ <span class="batches-section-count">{batches.passes.length}</span>
187
+ </div>
188
+ <div class="batches-table-head batches-table-head--passes">
189
+ <span class="batches-col-label">pass</span>
190
+ <span class="batches-col-num">calls</span>
191
+ <span class="batches-col-num">tris</span>
192
+ <span class="batches-col-num">ms</span>
193
+ </div>
194
+ <ul class="batches-pass-list">
195
+ {passRoots.length === 0 ? (
196
+ <li class="batches-empty">no passes captured</li>
197
+ ) : (
198
+ passRoots.map((root) => (
199
+ <PassRow
200
+ key={root.index}
201
+ node={root}
202
+ collapsed={collapsedPasses}
203
+ onToggle={toggleCollapsed}
204
+ />
205
+ ))
206
+ )}
207
+ </ul>
208
+ <div class="batches-total-row">
209
+ <span class="batches-col-label">total (root)</span>
210
+ <span class="batches-col-num">{passTotals.calls}</span>
211
+ <span class="batches-col-num">{formatTris(passTotals.tris)}</span>
212
+ <span class="batches-col-num">{passTotals.cpuMs.toFixed(2)}</span>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </section>
217
+ )
218
+ }
219
+
220
+ /**
221
+ * Recursive row renderer for the pass tree. A pass is "expandable"
222
+ * when it has children; clicking its caret toggles the subtree.
223
+ * Leaf passes render without a caret. Indentation scales with depth
224
+ * so the tree structure is obvious even without explicit tree lines.
225
+ */
226
+ function PassRow({
227
+ node,
228
+ collapsed,
229
+ onToggle,
230
+ }: {
231
+ node: PassNode
232
+ collapsed: Set<string>
233
+ onToggle: (label: string) => void
234
+ }) {
235
+ const { pass, children } = node
236
+ const hasChildren = children.length > 0
237
+ const isCollapsed = collapsed.has(pass.label)
238
+ const pad = 8 + pass.depth * 12
239
+
240
+ return (
241
+ <>
242
+ <li
243
+ class={`batches-pass-row${hasChildren ? ' batches-pass-row--parent' : ''}`}
244
+ style={{ paddingLeft: `${pad}px` }}
245
+ >
246
+ <span class="batches-col-label batches-pass-label">
247
+ {hasChildren ? (
248
+ <button
249
+ type="button"
250
+ class="batches-tree-caret"
251
+ aria-expanded={!isCollapsed}
252
+ onClick={() => onToggle(pass.label)}
253
+ >
254
+ {isCollapsed ? '▸' : '▾'}
255
+ </button>
256
+ ) : (
257
+ <span class="batches-tree-caret batches-tree-caret--leaf" />
258
+ )}
259
+ <span>{pass.label}</span>
260
+ </span>
261
+ <span class="batches-col-num">{pass.calls}</span>
262
+ <span class="batches-col-num">{formatTris(pass.triangles)}</span>
263
+ <span class="batches-col-num">{pass.cpuMs.toFixed(2)}</span>
264
+ </li>
265
+ {hasChildren && !isCollapsed
266
+ ? children.map((c) => (
267
+ <PassRow
268
+ key={c.index}
269
+ node={c}
270
+ collapsed={collapsed}
271
+ onToggle={onToggle}
272
+ />
273
+ ))
274
+ : null}
275
+ </>
276
+ )
277
+ }
278
+
279
+ function formatTris(n: number): string {
280
+ if (n >= 10_000) return `${(n / 1000).toFixed(1)}k`
281
+ return n.toString()
282
+ }