@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.
Files changed (172) hide show
  1. package/README.md +1 -1
  2. package/dist/buffers-modal.d.ts +16 -52
  3. package/dist/buffers-modal.d.ts.map +1 -0
  4. package/dist/buffers-modal.js +459 -464
  5. package/dist/buffers-modal.js.map +1 -1
  6. package/dist/buffers-view.d.ts +22 -34
  7. package/dist/buffers-view.d.ts.map +1 -0
  8. package/dist/buffers-view.js +252 -240
  9. package/dist/buffers-view.js.map +1 -1
  10. package/dist/create-pane.d.ts +38 -37
  11. package/dist/create-pane.d.ts.map +1 -0
  12. package/dist/create-pane.js +197 -166
  13. package/dist/create-pane.js.map +1 -1
  14. package/dist/dashboard/app.tsx +2 -0
  15. package/dist/dashboard/export.ts +1 -3
  16. package/dist/dashboard/frame-cursor.ts +81 -0
  17. package/dist/dashboard/hooks.ts +10 -2
  18. package/dist/dashboard/index.html +61 -0
  19. package/dist/dashboard/panels/batches.tsx +5 -18
  20. package/dist/dashboard/panels/buffers.tsx +52 -26
  21. package/dist/dashboard/panels/env.tsx +5 -6
  22. package/dist/dashboard/panels/protocol-log.tsx +146 -58
  23. package/dist/dashboard/panels/registry.tsx +55 -69
  24. package/dist/dashboard/panels/scrubber.tsx +100 -0
  25. package/dist/dashboard/panels/stats.tsx +60 -32
  26. package/dist/dashboard/protocol-store.ts +25 -8
  27. package/dist/dashboard/vendor/hooks.module.d.ts +146 -0
  28. package/dist/dashboard/vendor/jsx-runtime.d.ts +55 -0
  29. package/dist/dashboard/vendor/jsx.d.ts +3123 -0
  30. package/dist/dashboard/vendor/preact.module.d.ts +397 -0
  31. package/dist/devtools-client.d.ts +312 -300
  32. package/dist/devtools-client.d.ts.map +1 -0
  33. package/dist/devtools-client.js +866 -838
  34. package/dist/devtools-client.js.map +1 -1
  35. package/dist/index.d.ts +6 -8
  36. package/dist/index.js +5 -12
  37. package/dist/perf-trace.d.ts +6 -4
  38. package/dist/perf-trace.d.ts.map +1 -0
  39. package/dist/perf-trace.js +80 -66
  40. package/dist/perf-trace.js.map +1 -1
  41. package/dist/plugins.d.ts +6 -6
  42. package/dist/plugins.d.ts.map +1 -0
  43. package/dist/plugins.js +5 -5
  44. package/dist/plugins.js.map +1 -1
  45. package/dist/provider-switcher.d.ts +8 -15
  46. package/dist/provider-switcher.d.ts.map +1 -0
  47. package/dist/provider-switcher.js +68 -68
  48. package/dist/provider-switcher.js.map +1 -1
  49. package/dist/react/devtools-provider.d.ts +11 -10
  50. package/dist/react/devtools-provider.d.ts.map +1 -0
  51. package/dist/react/devtools-provider.js +60 -32
  52. package/dist/react/devtools-provider.js.map +1 -1
  53. package/dist/react/use-fps-graph.d.ts +7 -7
  54. package/dist/react/use-fps-graph.d.ts.map +1 -0
  55. package/dist/react/use-fps-graph.js +27 -21
  56. package/dist/react/use-fps-graph.js.map +1 -1
  57. package/dist/react/use-pane-button.d.ts +4 -4
  58. package/dist/react/use-pane-button.d.ts.map +1 -0
  59. package/dist/react/use-pane-button.js +35 -30
  60. package/dist/react/use-pane-button.js.map +1 -1
  61. package/dist/react/use-pane-folder.d.ts +5 -4
  62. package/dist/react/use-pane-folder.d.ts.map +1 -0
  63. package/dist/react/use-pane-folder.js +33 -20
  64. package/dist/react/use-pane-folder.js.map +1 -1
  65. package/dist/react/use-pane-input.d.ts +31 -30
  66. package/dist/react/use-pane-input.d.ts.map +1 -0
  67. package/dist/react/use-pane-input.js +46 -42
  68. package/dist/react/use-pane-input.js.map +1 -1
  69. package/dist/react/use-pane-radio-grid.d.ts +20 -20
  70. package/dist/react/use-pane-radio-grid.d.ts.map +1 -0
  71. package/dist/react/use-pane-radio-grid.js +72 -50
  72. package/dist/react/use-pane-radio-grid.js.map +1 -1
  73. package/dist/react/use-pane.d.ts +4 -4
  74. package/dist/react/use-pane.d.ts.map +1 -0
  75. package/dist/react/use-pane.js +52 -17
  76. package/dist/react/use-pane.js.map +1 -1
  77. package/dist/react.d.ts +10 -12
  78. package/dist/react.js +2 -12
  79. package/dist/registry-view.d.ts +8 -21
  80. package/dist/registry-view.d.ts.map +1 -0
  81. package/dist/registry-view.js +357 -338
  82. package/dist/registry-view.js.map +1 -1
  83. package/dist/relay.d.ts +16 -0
  84. package/dist/relay.d.ts.map +1 -0
  85. package/dist/relay.js +189 -0
  86. package/dist/relay.js.map +1 -0
  87. package/dist/remote.d.ts +10 -0
  88. package/dist/remote.d.ts.map +1 -0
  89. package/dist/remote.js +28 -0
  90. package/dist/remote.js.map +1 -0
  91. package/dist/stats-graph.d.ts +23 -31
  92. package/dist/stats-graph.d.ts.map +1 -0
  93. package/dist/stats-graph.js +323 -264
  94. package/dist/stats-graph.js.map +1 -1
  95. package/dist/stats-row.d.ts +8 -21
  96. package/dist/stats-row.d.ts.map +1 -0
  97. package/dist/stats-row.js +99 -101
  98. package/dist/stats-row.js.map +1 -1
  99. package/dist/theme.d.ts +3 -1
  100. package/dist/theme.d.ts.map +1 -0
  101. package/dist/theme.js +43 -54
  102. package/dist/theme.js.map +1 -1
  103. package/dist/vite-plugin.d.ts +7 -6
  104. package/dist/vite-plugin.d.ts.map +1 -0
  105. package/dist/vite-plugin.js +87 -78
  106. package/dist/vite-plugin.js.map +1 -1
  107. package/package.json +33 -29
  108. package/dist/buffers-modal.cjs +0 -491
  109. package/dist/buffers-modal.cjs.map +0 -1
  110. package/dist/buffers-modal.d.cts +0 -62
  111. package/dist/buffers-view.cjs +0 -269
  112. package/dist/buffers-view.cjs.map +0 -1
  113. package/dist/buffers-view.d.cts +0 -39
  114. package/dist/create-pane.cjs +0 -203
  115. package/dist/create-pane.cjs.map +0 -1
  116. package/dist/create-pane.d.cts +0 -66
  117. package/dist/devtools-client.cjs +0 -870
  118. package/dist/devtools-client.cjs.map +0 -1
  119. package/dist/devtools-client.d.cts +0 -362
  120. package/dist/index.cjs +0 -42
  121. package/dist/index.cjs.map +0 -1
  122. package/dist/index.d.cts +0 -8
  123. package/dist/index.js.map +0 -1
  124. package/dist/perf-trace.cjs +0 -103
  125. package/dist/perf-trace.cjs.map +0 -1
  126. package/dist/perf-trace.d.cts +0 -27
  127. package/dist/plugins.cjs +0 -44
  128. package/dist/plugins.cjs.map +0 -1
  129. package/dist/plugins.d.cts +0 -7
  130. package/dist/provider-switcher.cjs +0 -95
  131. package/dist/provider-switcher.cjs.map +0 -1
  132. package/dist/provider-switcher.d.cts +0 -18
  133. package/dist/react/devtools-provider.cjs +0 -59
  134. package/dist/react/devtools-provider.cjs.map +0 -1
  135. package/dist/react/devtools-provider.d.cts +0 -39
  136. package/dist/react/use-fps-graph.cjs +0 -49
  137. package/dist/react/use-fps-graph.cjs.map +0 -1
  138. package/dist/react/use-fps-graph.d.cts +0 -15
  139. package/dist/react/use-pane-button.cjs +0 -58
  140. package/dist/react/use-pane-button.cjs.map +0 -1
  141. package/dist/react/use-pane-button.d.cts +0 -10
  142. package/dist/react/use-pane-folder.cjs +0 -48
  143. package/dist/react/use-pane-folder.cjs.map +0 -1
  144. package/dist/react/use-pane-folder.d.cts +0 -15
  145. package/dist/react/use-pane-input.cjs +0 -70
  146. package/dist/react/use-pane-input.cjs.map +0 -1
  147. package/dist/react/use-pane-input.d.cts +0 -44
  148. package/dist/react/use-pane-radio-grid.cjs +0 -78
  149. package/dist/react/use-pane-radio-grid.cjs.map +0 -1
  150. package/dist/react/use-pane-radio-grid.d.cts +0 -35
  151. package/dist/react/use-pane.cjs +0 -45
  152. package/dist/react/use-pane.cjs.map +0 -1
  153. package/dist/react/use-pane.d.cts +0 -40
  154. package/dist/react.cjs +0 -50
  155. package/dist/react.cjs.map +0 -1
  156. package/dist/react.d.cts +0 -12
  157. package/dist/react.js.map +0 -1
  158. package/dist/registry-view.cjs +0 -371
  159. package/dist/registry-view.cjs.map +0 -1
  160. package/dist/registry-view.d.cts +0 -24
  161. package/dist/stats-graph.cjs +0 -297
  162. package/dist/stats-graph.cjs.map +0 -1
  163. package/dist/stats-graph.d.cts +0 -40
  164. package/dist/stats-row.cjs +0 -153
  165. package/dist/stats-row.cjs.map +0 -1
  166. package/dist/stats-row.d.cts +0 -24
  167. package/dist/theme.cjs +0 -260
  168. package/dist/theme.cjs.map +0 -1
  169. package/dist/theme.d.cts +0 -10
  170. package/dist/vite-plugin.cjs +0 -113
  171. package/dist/vite-plugin.cjs.map +0 -1
  172. package/dist/vite-plugin.d.cts +0 -9
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/registry-view.ts"],"sourcesContent":["/**\n * Registry view — one group visible at a time. Header reads\n * `◀ groupName ▶`; arrows cycle through the groups the provider has\n * published. Below the header, every entry in the active group is\n * stacked (one bar chart per entry) so you see the full module in one\n * glance. Hidden while the registry is empty.\n *\n * Groups are derived from the part of each entry name before the\n * first `.` (`forwardPlus.lightCounts` + `forwardPlus.tileScores` →\n * group `forwardPlus`). Names without a dot land in an `ungrouped`\n * bucket.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\n\nimport type { DevtoolsClient, RegistryEntrySnapshot } from './devtools-client.js'\n\nexport interface RegistryViewHandle {\n readonly element: HTMLElement\n dispose(): void\n}\n\ninterface EntryRow {\n name: string\n shortName: string\n row: HTMLDivElement\n nameLabel: HTMLSpanElement\n valueLabel: HTMLSpanElement\n canvas: HTMLCanvasElement\n ctx: CanvasRenderingContext2D | null\n lastVersion: number\n}\n\ninterface GroupUI {\n group: string\n container: HTMLDivElement\n entries: Map<string, EntryRow>\n}\n\nconst BAR_HEIGHT = 24\nconst BAR_WIDTH = 240\n\nconst FILL_BY_KIND: Record<string, string> = {\n float: '#47cca9',\n int: '#47cc6a',\n uint: '#ffa347',\n bits: '#d94c87',\n float2: '#9d7aff',\n float3: '#9d7aff',\n float4: '#9d7aff',\n}\n\nfunction splitName(name: string): { group: string; short: string } {\n const dot = name.indexOf('.')\n if (dot === -1) return { group: 'ungrouped', short: name }\n return { group: name.slice(0, dot), short: name.slice(dot + 1) }\n}\n\nexport function addRegistryView(\n parent: Pane | FolderApi,\n client: DevtoolsClient,\n): RegistryViewHandle {\n const blade = parent.addBlade({ view: 'separator' }) as unknown as {\n element: HTMLElement\n dispose(): void\n }\n const bladeEl = blade.element\n bladeEl.innerHTML = ''\n bladeEl.className = 'tp-cntv'\n // Hidden until entries exist. Set `display:flex` (not `''`) when\n // showing so the children can centre-align via flex rules — toggling\n // to '' would revert to the stylesheet's `block`, breaking layout.\n // `background` lays a ~30% black overlay over whatever Tweakpane's\n // base colour is, visually sinking this blade back relative to the\n // stats blades above it.\n bladeEl.style.cssText = 'display:none;flex-direction:column;background:rgba(0,0,0,0.3)'\n\n // Header: \"◀ group ▶\" — group name centred, arrows on either end.\n // Clicking the centre label toggles collapse. No fold chevron — the\n // visible body IS the indicator.\n const header = document.createElement('div')\n // Symmetric 2em arrow columns so the 1fr middle is the blade's true\n // centre regardless of arrow-glyph rendering width. `cursor:pointer`\n // on the whole header so the collapse target isn't just the text\n // glyphs; arrows stop propagation so they don't also toggle.\n header.style.cssText = [\n 'display:grid',\n 'grid-template-columns:2em 1fr 2em',\n 'align-items:center',\n 'padding:4px 6px 6px',\n 'font-size:11px',\n 'color:var(--tp-label-foreground-color)',\n 'user-select:none',\n '-webkit-user-select:none',\n 'font-variant-numeric:tabular-nums',\n 'cursor:pointer',\n ].join(';')\n // Match the provider-switcher arrow styling verbatim so both cycle\n // controls feel identical. `padding:0 4px` gives a more generous\n // click target than relying on the grid cell alone.\n const arrowStyle = 'cursor:pointer;padding:0 4px;opacity:0.8;font-family:ui-monospace,monospace;text-align:center'\n const prevBtn = document.createElement('span')\n prevBtn.textContent = '◀'\n prevBtn.style.cssText = arrowStyle\n prevBtn.setAttribute('role', 'button')\n prevBtn.setAttribute('aria-label', 'Previous registry group')\n const groupLabel = document.createElement('span')\n // Intentionally no `cursor:pointer` here — the click handler lives on\n // the header row, so the whole row shares one hit target. Giving the\n // label its own cursor/handler caused inconsistent hit-testing when\n // the user clicked on the label's text-vs-padding boundary.\n groupLabel.style.cssText = 'font-weight:500;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none'\n const nextBtn = document.createElement('span')\n nextBtn.textContent = '▶'\n nextBtn.style.cssText = arrowStyle\n nextBtn.setAttribute('role', 'button')\n nextBtn.setAttribute('aria-label', 'Next registry group')\n header.appendChild(prevBtn)\n header.appendChild(groupLabel)\n header.appendChild(nextBtn)\n\n const body = document.createElement('div')\n body.style.cssText = 'display:flex;flex-direction:column;gap:4px;padding:2px 6px 6px'\n\n bladeEl.appendChild(header)\n bladeEl.appendChild(body)\n\n const groups = new Map<string, GroupUI>()\n let activeGroup: string | null = null\n // Start collapsed — don't stream registry data until the user opens\n // the view. `setRegistry([])` below keeps the provider quiet.\n let collapsed = true\n body.style.display = 'none'\n\n /** Push the currently-visible entry names up to the client. */\n function syncFilter(): void {\n if (collapsed || activeGroup === null) {\n client.setRegistry([])\n return\n }\n const g = groups.get(activeGroup)\n if (!g) {\n client.setRegistry([])\n return\n }\n const names: string[] = []\n for (const row of g.entries.values()) names.push(row.name)\n client.setRegistry(names)\n }\n\n const toggleCollapse = (e: Event): void => {\n // Stop propagation so the separator-blade shim below doesn't ever\n // see this click (Tweakpane can sometimes process blade pointer\n // events in a way that interferes with custom handlers).\n e.stopPropagation()\n collapsed = !collapsed\n body.style.display = collapsed ? 'none' : 'flex'\n if (!collapsed && activeGroup !== null) {\n const g = groups.get(activeGroup)\n if (g) {\n g.container.style.display = 'flex'\n for (const row of g.entries.values()) row.lastVersion = -1\n }\n }\n syncFilter()\n }\n header.addEventListener('click', toggleCollapse)\n prevBtn.addEventListener('click', (e) => { e.stopPropagation(); cycle(-1) })\n nextBtn.addEventListener('click', (e) => { e.stopPropagation(); cycle(1) })\n\n function ensureGroup(name: string): GroupUI {\n let g = groups.get(name)\n if (g) return g\n const container = document.createElement('div')\n container.style.cssText = 'display:none;flex-direction:column;gap:4px'\n body.appendChild(container)\n g = { group: name, container, entries: new Map() }\n groups.set(name, g)\n return g\n }\n\n function destroyGroup(name: string): void {\n const g = groups.get(name)\n if (!g) return\n g.container.remove()\n groups.delete(name)\n if (activeGroup === name) activeGroup = null\n }\n\n function ensureEntryRow(g: GroupUI, name: string, shortName: string): EntryRow {\n const existing = g.entries.get(shortName)\n if (existing) return existing\n\n const row = document.createElement('div')\n row.style.cssText = 'display:flex;flex-direction:column;gap:2px'\n\n const head = document.createElement('div')\n head.style.cssText = [\n 'display:flex',\n 'align-items:baseline',\n 'gap:6px',\n 'font-size:10px',\n 'line-height:1.2',\n 'font-family:var(--tp-base-font-family, ui-monospace, monospace)',\n 'font-variant-numeric:tabular-nums',\n 'color:var(--tp-monitor-foreground-color)',\n ].join(';')\n const nameLabel = document.createElement('span')\n nameLabel.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--tp-label-foreground-color)'\n nameLabel.textContent = shortName\n const valueLabel = document.createElement('span')\n valueLabel.style.cssText = 'opacity:0.75;white-space:nowrap'\n head.appendChild(nameLabel)\n head.appendChild(valueLabel)\n\n const canvas = document.createElement('canvas')\n canvas.width = BAR_WIDTH\n canvas.height = BAR_HEIGHT\n canvas.style.cssText = `width:100%;height:${BAR_HEIGHT}px;background:rgba(0,2,28,0.45);border-radius:2px`\n\n row.appendChild(head)\n row.appendChild(canvas)\n g.container.appendChild(row)\n\n const entry: EntryRow = {\n name,\n shortName,\n row,\n nameLabel,\n valueLabel,\n canvas,\n ctx: canvas.getContext('2d'),\n lastVersion: -1,\n }\n g.entries.set(shortName, entry)\n return entry\n }\n\n function destroyEntryRow(g: GroupUI, shortName: string): void {\n const row = g.entries.get(shortName)\n if (!row) return\n row.row.remove()\n g.entries.delete(shortName)\n }\n\n function setActiveGroup(name: string | null): void {\n activeGroup = name\n for (const [groupName, g] of groups) {\n g.container.style.display = groupName === name && !collapsed ? 'flex' : 'none'\n }\n groupLabel.textContent = name ?? '—'\n if (name !== null) {\n const g = groups.get(name)\n if (g) for (const row of g.entries.values()) row.lastVersion = -1\n }\n syncFilter()\n }\n\n function cycle(delta: number): void {\n const names = Array.from(groups.keys())\n if (names.length === 0) return\n const idx = activeGroup !== null ? names.indexOf(activeGroup) : -1\n const next = names[(idx + delta + names.length) % names.length]!\n setActiveGroup(next)\n }\n // (arrow click handlers wired above, alongside the toggle.)\n\n function renderBars(row: EntryRow, kind: string, sample: Float32Array | Uint32Array | Int32Array, isSigned: boolean): { min: number; max: number; mean: number } {\n const ctx = row.ctx\n const w = row.canvas.width\n const h = row.canvas.height\n let min = Infinity\n let max = -Infinity\n let sum = 0\n const n = sample.length\n for (let i = 0; i < n; i++) {\n const v = sample[i]!\n if (v < min) min = v\n if (v > max) max = v\n sum += v\n }\n const mean = n > 0 ? sum / n : 0\n\n if (!ctx) return { min, max, mean }\n ctx.clearRect(0, 0, w, h)\n if (n === 0 || !Number.isFinite(min) || !Number.isFinite(max)) return { min, max: 0, mean }\n\n const zeroY = isSigned ? h / 2 : h\n const scale = isSigned ? (h / 2) / Math.max(Math.abs(min), Math.abs(max) || 1) : h / (max || 1)\n\n ctx.fillStyle = FILL_BY_KIND[kind] ?? '#47cca9'\n const bw = Math.max(1, w / n)\n for (let i = 0; i < n; i++) {\n const v = sample[i]!\n const x = Math.floor((i / n) * w)\n const bh = v * scale\n if (isSigned) {\n if (bh >= 0) ctx.fillRect(x, zeroY - bh, Math.ceil(bw), bh)\n else ctx.fillRect(x, zeroY, Math.ceil(bw), -bh)\n } else {\n ctx.fillRect(x, zeroY - bh, Math.ceil(bw), bh)\n }\n }\n if (isSigned) {\n ctx.fillStyle = 'rgba(240, 237, 216, 0.2)'\n ctx.fillRect(0, zeroY, w, 1)\n }\n return { min, max, mean }\n }\n\n function renderBits(row: EntryRow, sample: Uint32Array, count: number): { ones: number; total: number } {\n const ctx = row.ctx\n const w = row.canvas.width\n const h = row.canvas.height\n if (!ctx) return { ones: 0, total: 0 }\n ctx.clearRect(0, 0, w, h)\n const totalBits = count * 32\n if (totalBits === 0) return { ones: 0, total: 0 }\n let ones = 0\n const px = w / totalBits\n ctx.fillStyle = FILL_BY_KIND.bits!\n for (let i = 0; i < count; i++) {\n const word = sample[i]! >>> 0\n for (let b = 0; b < 32; b++) {\n if ((word >>> b) & 1) {\n ones++\n const bitIdx = i * 32 + b\n const x = Math.floor(bitIdx * px)\n ctx.fillRect(x, 2, Math.max(1, Math.ceil(px)), h - 4)\n }\n }\n }\n return { ones, total: totalBits }\n }\n\n function renderEntry(row: EntryRow, entry: RegistryEntrySnapshot): void {\n if (row.lastVersion === entry.version) return\n row.lastVersion = entry.version\n row.nameLabel.title = `${entry.name} · ${entry.kind} · ${entry.count}`\n\n const kind = entry.kind\n if (kind === 'bits') {\n const { ones, total } = renderBits(row, entry.sample as Uint32Array, entry.count)\n row.valueLabel.textContent = `${ones} / ${total}`\n return\n }\n\n if (kind === 'float2' || kind === 'float3' || kind === 'float4') {\n const stride = kind === 'float2' ? 2 : kind === 'float3' ? 3 : 4\n const n = Math.floor(entry.sample.length / stride)\n const mags = new Float32Array(n)\n for (let i = 0; i < n; i++) {\n let sq = 0\n for (let c = 0; c < stride; c++) {\n const v = entry.sample[i * stride + c]!\n sq += v * v\n }\n mags[i] = Math.sqrt(sq)\n }\n const stats = renderBars(row, kind, mags, false)\n row.valueLabel.textContent = `${n}×${stride} μ=${stats.mean.toFixed(2)}`\n return\n }\n\n const isSigned = kind === 'int' || kind === 'float'\n const stats = renderBars(row, kind, entry.sample, isSigned)\n const fmt = (v: number): string => {\n if (!Number.isFinite(v)) return '—'\n if (kind === 'uint' || kind === 'int') return Math.round(v).toString()\n return v.toFixed(2)\n }\n row.valueLabel.textContent = `μ=${fmt(stats.mean)} ${fmt(stats.min)}–${fmt(stats.max)}`\n }\n\n const unsubscribe = client.addListener((state) => {\n const registry = state.registry\n\n const seen = new Map<string, Set<string>>()\n for (const [name] of registry) {\n const { group, short } = splitName(name)\n let bucket = seen.get(group)\n if (!bucket) {\n bucket = new Set()\n seen.set(group, bucket)\n }\n bucket.add(short)\n }\n\n // Prune.\n for (const [groupName, g] of groups) {\n const bucket = seen.get(groupName)\n if (!bucket) {\n destroyGroup(groupName)\n continue\n }\n for (const shortName of g.entries.keys()) {\n if (!bucket.has(shortName)) destroyEntryRow(g, shortName)\n }\n }\n\n // Ensure every row exists first (so cycling never lands on a\n // missing container), but defer rendering until active group is\n // pinned below.\n for (const [name] of registry) {\n const { group, short } = splitName(name)\n const g = ensureGroup(group)\n ensureEntryRow(g, name, short)\n }\n\n // Establish / repair active group.\n if (groups.size === 0) {\n activeGroup = null\n groupLabel.textContent = '—'\n } else if (activeGroup === null || !groups.has(activeGroup)) {\n setActiveGroup(groups.keys().next().value as string)\n } else {\n groupLabel.textContent = activeGroup\n const g = groups.get(activeGroup)!\n g.container.style.display = collapsed ? 'none' : 'flex'\n }\n\n // Only re-render the rows the user can actually see. Inactive\n // groups + collapsed state = no canvas work at all.\n if (!collapsed && activeGroup !== null) {\n const g = groups.get(activeGroup)\n if (g) {\n for (const [shortName, row] of g.entries) {\n const entry = registry.get(`${activeGroup}.${shortName}`) ?? registry.get(shortName)\n if (entry) renderEntry(row, entry)\n }\n }\n }\n\n // Arrows stay visible (so the header stays symmetric) but dim when\n // there's nowhere to cycle to.\n const multi = groups.size > 1\n const arrowOpacity = multi ? '0.8' : '0.25'\n const arrowPointer = multi ? 'pointer' : 'default'\n prevBtn.style.opacity = arrowOpacity\n nextBtn.style.opacity = arrowOpacity\n prevBtn.style.cursor = arrowPointer\n nextBtn.style.cursor = arrowPointer\n\n // Use `flex` (not `''`) so children can centre-align via flex.\n bladeEl.style.display = registry.size > 0 ? 'flex' : 'none'\n\n // Resync the filter — an entry may have been added/removed in the\n // active group since the last call. Client dedupes identical\n // subscribes so this is cheap on the quiet path.\n syncFilter()\n })\n\n return {\n element: bladeEl,\n dispose() {\n unsubscribe()\n groups.clear()\n blade.dispose()\n },\n }\n}\n"],"mappings":"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","names":["stats"]}
1
+ {"version":3,"file":"registry-view.js","names":[],"sources":["../src/registry-view.ts"],"sourcesContent":["/**\n * Registry view — one group visible at a time. Header reads\n * `◀ groupName ▶`; arrows cycle through the groups the provider has\n * published. Below the header, every entry in the active group is\n * stacked (one bar chart per entry) so you see the full module in one\n * glance. Hidden while the registry is empty.\n *\n * Groups are derived from the part of each entry name before the\n * first `.` (`forwardPlus.lightCounts` + `forwardPlus.tileScores` →\n * group `forwardPlus`). Names without a dot land in an `ungrouped`\n * bucket.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\n\nimport type { DevtoolsClient, RegistryEntrySnapshot } from './devtools-client.js'\n\nexport interface RegistryViewHandle {\n readonly element: HTMLElement\n dispose(): void\n}\n\ninterface EntryRow {\n name: string\n shortName: string\n row: HTMLDivElement\n nameLabel: HTMLSpanElement\n valueLabel: HTMLSpanElement\n canvas: HTMLCanvasElement\n ctx: CanvasRenderingContext2D | null\n lastVersion: number\n}\n\ninterface GroupUI {\n group: string\n container: HTMLDivElement\n entries: Map<string, EntryRow>\n}\n\nconst BAR_HEIGHT = 24\nconst BAR_WIDTH = 240\n\nconst FILL_BY_KIND: Record<string, string> = {\n float: '#47cca9',\n int: '#47cc6a',\n uint: '#ffa347',\n bits: '#d94c87',\n float2: '#9d7aff',\n float3: '#9d7aff',\n float4: '#9d7aff',\n}\n\nfunction splitName(name: string): { group: string; short: string } {\n const dot = name.indexOf('.')\n if (dot === -1) return { group: 'ungrouped', short: name }\n return { group: name.slice(0, dot), short: name.slice(dot + 1) }\n}\n\nexport function addRegistryView(parent: Pane | FolderApi, client: DevtoolsClient): RegistryViewHandle {\n const blade = parent.addBlade({ view: 'separator' }) as unknown as {\n element: HTMLElement\n dispose(): void\n }\n const bladeEl = blade.element\n bladeEl.innerHTML = ''\n bladeEl.className = 'tp-cntv'\n // Hidden until entries exist. Set `display:flex` (not `''`) when\n // showing so the children can centre-align via flex rules — toggling\n // to '' would revert to the stylesheet's `block`, breaking layout.\n // `background` lays a ~30% black overlay over whatever Tweakpane's\n // base colour is, visually sinking this blade back relative to the\n // stats blades above it.\n bladeEl.style.cssText = 'display:none;flex-direction:column;background:rgba(0,0,0,0.3)'\n\n // Header: \"◀ group ▶\" — group name centred, arrows on either end.\n // Clicking the centre label toggles collapse. No fold chevron — the\n // visible body IS the indicator.\n const header = document.createElement('div')\n // Symmetric 2em arrow columns so the 1fr middle is the blade's true\n // centre regardless of arrow-glyph rendering width. `cursor:pointer`\n // on the whole header so the collapse target isn't just the text\n // glyphs; arrows stop propagation so they don't also toggle.\n header.style.cssText = [\n 'display:grid',\n 'grid-template-columns:2em 1fr 2em',\n 'align-items:center',\n 'padding:4px 6px 6px',\n 'font-size:11px',\n 'color:var(--tp-label-foreground-color)',\n 'user-select:none',\n '-webkit-user-select:none',\n 'font-variant-numeric:tabular-nums',\n 'cursor:pointer',\n ].join(';')\n // Match the provider-switcher arrow styling verbatim so both cycle\n // controls feel identical. `padding:0 4px` gives a more generous\n // click target than relying on the grid cell alone.\n const arrowStyle = 'cursor:pointer;padding:0 4px;opacity:0.8;font-family:ui-monospace,monospace;text-align:center'\n const prevBtn = document.createElement('span')\n prevBtn.textContent = '◀'\n prevBtn.style.cssText = arrowStyle\n prevBtn.setAttribute('role', 'button')\n prevBtn.setAttribute('aria-label', 'Previous registry group')\n const groupLabel = document.createElement('span')\n // Intentionally no `cursor:pointer` here — the click handler lives on\n // the header row, so the whole row shares one hit target. Giving the\n // label its own cursor/handler caused inconsistent hit-testing when\n // the user clicked on the label's text-vs-padding boundary.\n groupLabel.style.cssText =\n 'font-weight:500;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none'\n const nextBtn = document.createElement('span')\n nextBtn.textContent = '▶'\n nextBtn.style.cssText = arrowStyle\n nextBtn.setAttribute('role', 'button')\n nextBtn.setAttribute('aria-label', 'Next registry group')\n header.appendChild(prevBtn)\n header.appendChild(groupLabel)\n header.appendChild(nextBtn)\n\n const body = document.createElement('div')\n body.style.cssText = 'display:flex;flex-direction:column;gap:4px;padding:2px 6px 6px'\n\n bladeEl.appendChild(header)\n bladeEl.appendChild(body)\n\n const groups = new Map<string, GroupUI>()\n let activeGroup: string | null = null\n // Start collapsed — don't stream registry data until the user opens\n // the view. `setRegistry([])` below keeps the provider quiet.\n let collapsed = true\n body.style.display = 'none'\n\n /** Push the currently-visible entry names up to the client. */\n function syncFilter(): void {\n if (collapsed || activeGroup === null) {\n client.setRegistry([])\n return\n }\n const g = groups.get(activeGroup)\n if (!g) {\n client.setRegistry([])\n return\n }\n const names: string[] = []\n for (const row of g.entries.values()) names.push(row.name)\n client.setRegistry(names)\n }\n\n const toggleCollapse = (e: Event): void => {\n // Stop propagation so the separator-blade shim below doesn't ever\n // see this click (Tweakpane can sometimes process blade pointer\n // events in a way that interferes with custom handlers).\n e.stopPropagation()\n collapsed = !collapsed\n body.style.display = collapsed ? 'none' : 'flex'\n if (!collapsed && activeGroup !== null) {\n const g = groups.get(activeGroup)\n if (g) {\n g.container.style.display = 'flex'\n for (const row of g.entries.values()) row.lastVersion = -1\n }\n }\n syncFilter()\n }\n header.addEventListener('click', toggleCollapse)\n prevBtn.addEventListener('click', (e) => {\n e.stopPropagation()\n cycle(-1)\n })\n nextBtn.addEventListener('click', (e) => {\n e.stopPropagation()\n cycle(1)\n })\n\n function ensureGroup(name: string): GroupUI {\n let g = groups.get(name)\n if (g) return g\n const container = document.createElement('div')\n container.style.cssText = 'display:none;flex-direction:column;gap:4px'\n body.appendChild(container)\n g = { group: name, container, entries: new Map() }\n groups.set(name, g)\n return g\n }\n\n function destroyGroup(name: string): void {\n const g = groups.get(name)\n if (!g) return\n g.container.remove()\n groups.delete(name)\n if (activeGroup === name) activeGroup = null\n }\n\n function ensureEntryRow(g: GroupUI, name: string, shortName: string): EntryRow {\n const existing = g.entries.get(shortName)\n if (existing) return existing\n\n const row = document.createElement('div')\n row.style.cssText = 'display:flex;flex-direction:column;gap:2px'\n\n const head = document.createElement('div')\n head.style.cssText = [\n 'display:flex',\n 'align-items:baseline',\n 'gap:6px',\n 'font-size:10px',\n 'line-height:1.2',\n 'font-family:var(--tp-base-font-family, ui-monospace, monospace)',\n 'font-variant-numeric:tabular-nums',\n 'color:var(--tp-monitor-foreground-color)',\n ].join(';')\n const nameLabel = document.createElement('span')\n nameLabel.style.cssText =\n 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--tp-label-foreground-color)'\n nameLabel.textContent = shortName\n const valueLabel = document.createElement('span')\n valueLabel.style.cssText = 'opacity:0.75;white-space:nowrap'\n head.appendChild(nameLabel)\n head.appendChild(valueLabel)\n\n const canvas = document.createElement('canvas')\n canvas.width = BAR_WIDTH\n canvas.height = BAR_HEIGHT\n canvas.style.cssText = `width:100%;height:${BAR_HEIGHT}px;background:rgba(0,2,28,0.45);border-radius:2px`\n\n row.appendChild(head)\n row.appendChild(canvas)\n g.container.appendChild(row)\n\n const entry: EntryRow = {\n name,\n shortName,\n row,\n nameLabel,\n valueLabel,\n canvas,\n ctx: canvas.getContext('2d'),\n lastVersion: -1,\n }\n g.entries.set(shortName, entry)\n return entry\n }\n\n function destroyEntryRow(g: GroupUI, shortName: string): void {\n const row = g.entries.get(shortName)\n if (!row) return\n row.row.remove()\n g.entries.delete(shortName)\n }\n\n function setActiveGroup(name: string | null): void {\n activeGroup = name\n for (const [groupName, g] of groups) {\n g.container.style.display = groupName === name && !collapsed ? 'flex' : 'none'\n }\n groupLabel.textContent = name ?? '—'\n if (name !== null) {\n const g = groups.get(name)\n if (g) for (const row of g.entries.values()) row.lastVersion = -1\n }\n syncFilter()\n }\n\n function cycle(delta: number): void {\n const names = Array.from(groups.keys())\n if (names.length === 0) return\n const idx = activeGroup !== null ? names.indexOf(activeGroup) : -1\n const next = names[(idx + delta + names.length) % names.length]!\n setActiveGroup(next)\n }\n // (arrow click handlers wired above, alongside the toggle.)\n\n function renderBars(\n row: EntryRow,\n kind: string,\n sample: Float32Array | Uint32Array | Int32Array,\n isSigned: boolean\n ): { min: number; max: number; mean: number } {\n const ctx = row.ctx\n const w = row.canvas.width\n const h = row.canvas.height\n let min = Infinity\n let max = -Infinity\n let sum = 0\n const n = sample.length\n for (let i = 0; i < n; i++) {\n const v = sample[i]!\n if (v < min) min = v\n if (v > max) max = v\n sum += v\n }\n const mean = n > 0 ? sum / n : 0\n\n if (!ctx) return { min, max, mean }\n ctx.clearRect(0, 0, w, h)\n if (n === 0 || !Number.isFinite(min) || !Number.isFinite(max)) return { min, max: 0, mean }\n\n const zeroY = isSigned ? h / 2 : h\n const scale = isSigned ? h / 2 / Math.max(Math.abs(min), Math.abs(max) || 1) : h / (max || 1)\n\n ctx.fillStyle = FILL_BY_KIND[kind] ?? '#47cca9'\n const bw = Math.max(1, w / n)\n for (let i = 0; i < n; i++) {\n const v = sample[i]!\n const x = Math.floor((i / n) * w)\n const bh = v * scale\n if (isSigned) {\n if (bh >= 0) ctx.fillRect(x, zeroY - bh, Math.ceil(bw), bh)\n else ctx.fillRect(x, zeroY, Math.ceil(bw), -bh)\n } else {\n ctx.fillRect(x, zeroY - bh, Math.ceil(bw), bh)\n }\n }\n if (isSigned) {\n ctx.fillStyle = 'rgba(240, 237, 216, 0.2)'\n ctx.fillRect(0, zeroY, w, 1)\n }\n return { min, max, mean }\n }\n\n function renderBits(row: EntryRow, sample: Uint32Array, count: number): { ones: number; total: number } {\n const ctx = row.ctx\n const w = row.canvas.width\n const h = row.canvas.height\n if (!ctx) return { ones: 0, total: 0 }\n ctx.clearRect(0, 0, w, h)\n const totalBits = count * 32\n if (totalBits === 0) return { ones: 0, total: 0 }\n let ones = 0\n const px = w / totalBits\n ctx.fillStyle = FILL_BY_KIND.bits!\n for (let i = 0; i < count; i++) {\n const word = sample[i]! >>> 0\n for (let b = 0; b < 32; b++) {\n if ((word >>> b) & 1) {\n ones++\n const bitIdx = i * 32 + b\n const x = Math.floor(bitIdx * px)\n ctx.fillRect(x, 2, Math.max(1, Math.ceil(px)), h - 4)\n }\n }\n }\n return { ones, total: totalBits }\n }\n\n function renderEntry(row: EntryRow, entry: RegistryEntrySnapshot): void {\n if (row.lastVersion === entry.version) return\n row.lastVersion = entry.version\n row.nameLabel.title = `${entry.name} · ${entry.kind} · ${entry.count}`\n\n const kind = entry.kind\n if (kind === 'bits') {\n const { ones, total } = renderBits(row, entry.sample as Uint32Array, entry.count)\n row.valueLabel.textContent = `${ones} / ${total}`\n return\n }\n\n if (kind === 'float2' || kind === 'float3' || kind === 'float4') {\n const stride = kind === 'float2' ? 2 : kind === 'float3' ? 3 : 4\n const n = Math.floor(entry.sample.length / stride)\n const mags = new Float32Array(n)\n for (let i = 0; i < n; i++) {\n let sq = 0\n for (let c = 0; c < stride; c++) {\n const v = entry.sample[i * stride + c]!\n sq += v * v\n }\n mags[i] = Math.sqrt(sq)\n }\n const stats = renderBars(row, kind, mags, false)\n row.valueLabel.textContent = `${n}×${stride} μ=${stats.mean.toFixed(2)}`\n return\n }\n\n const isSigned = kind === 'int' || kind === 'float'\n const stats = renderBars(row, kind, entry.sample, isSigned)\n const fmt = (v: number): string => {\n if (!Number.isFinite(v)) return '—'\n if (kind === 'uint' || kind === 'int') return Math.round(v).toString()\n return v.toFixed(2)\n }\n row.valueLabel.textContent = `μ=${fmt(stats.mean)} ${fmt(stats.min)}–${fmt(stats.max)}`\n }\n\n const unsubscribe = client.addListener((state) => {\n const registry = state.registry\n\n const seen = new Map<string, Set<string>>()\n for (const [name] of registry) {\n const { group, short } = splitName(name)\n let bucket = seen.get(group)\n if (!bucket) {\n bucket = new Set()\n seen.set(group, bucket)\n }\n bucket.add(short)\n }\n\n // Prune.\n for (const [groupName, g] of groups) {\n const bucket = seen.get(groupName)\n if (!bucket) {\n destroyGroup(groupName)\n continue\n }\n for (const shortName of g.entries.keys()) {\n if (!bucket.has(shortName)) destroyEntryRow(g, shortName)\n }\n }\n\n // Ensure every row exists first (so cycling never lands on a\n // missing container), but defer rendering until active group is\n // pinned below.\n for (const [name] of registry) {\n const { group, short } = splitName(name)\n const g = ensureGroup(group)\n ensureEntryRow(g, name, short)\n }\n\n // Establish / repair active group.\n if (groups.size === 0) {\n activeGroup = null\n groupLabel.textContent = '—'\n } else if (activeGroup === null || !groups.has(activeGroup)) {\n setActiveGroup(groups.keys().next().value as string)\n } else {\n groupLabel.textContent = activeGroup\n const g = groups.get(activeGroup)!\n g.container.style.display = collapsed ? 'none' : 'flex'\n }\n\n // Only re-render the rows the user can actually see. Inactive\n // groups + collapsed state = no canvas work at all.\n if (!collapsed && activeGroup !== null) {\n const g = groups.get(activeGroup)\n if (g) {\n for (const [shortName, row] of g.entries) {\n const entry = registry.get(`${activeGroup}.${shortName}`) ?? registry.get(shortName)\n if (entry) renderEntry(row, entry)\n }\n }\n }\n\n // Arrows stay visible (so the header stays symmetric) but dim when\n // there's nowhere to cycle to.\n const multi = groups.size > 1\n const arrowOpacity = multi ? '0.8' : '0.25'\n const arrowPointer = multi ? 'pointer' : 'default'\n prevBtn.style.opacity = arrowOpacity\n nextBtn.style.opacity = arrowOpacity\n prevBtn.style.cursor = arrowPointer\n nextBtn.style.cursor = arrowPointer\n\n // Use `flex` (not `''`) so children can centre-align via flex.\n bladeEl.style.display = registry.size > 0 ? 'flex' : 'none'\n\n // Resync the filter — an entry may have been added/removed in the\n // active group since the last call. Client dedupes identical\n // subscribes so this is cheap on the quiet path.\n syncFilter()\n })\n\n return {\n element: bladeEl,\n dispose() {\n unsubscribe()\n groups.clear()\n blade.dispose()\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { Server } from "node:http";
2
+ //#region src/relay.d.ts
3
+ interface RelayHandle {
4
+ /** Stop the relay: destroys all client sockets and closes the server. */
5
+ close: () => void;
6
+ /**
7
+ * The underlying HTTP server — use `.address()` to read the bound
8
+ * port when `port` was passed as 0 (ephemeral; tests only, the CLI
9
+ * always passes an explicit port).
10
+ */
11
+ server: Server;
12
+ }
13
+ declare function startRelay(port: number, host: string): RelayHandle;
14
+ //#endregion
15
+ export { RelayHandle, startRelay };
16
+ //# sourceMappingURL=relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.d.ts","names":[],"sources":["../src/relay.ts"],"mappings":";;UAkCiB;;EAEf;;;;;;EAMA,QAAQ;;iBAGM,WAAW,cAAc,eAAe"}
package/dist/relay.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from "node:http";
3
+ import { createHash } from "node:crypto";
4
+ //#region src/relay.ts
5
+ /**
6
+ * flatland-devtools-relay — minimal WebSocket relay for remote/mobile
7
+ * debugging (#114). Every frame received from one client is forwarded
8
+ * to every other client; the provider bridge and the dashboard bridge
9
+ * sort out semantics via the bus-frame direction sets.
10
+ *
11
+ * npx flatland-devtools-relay [--port 8123] [--host 0.0.0.0]
12
+ *
13
+ * Zero dependencies: a hand-rolled RFC 6455 server covering exactly
14
+ * what the bridges use — frames ≤ 16 MB (single frame or reassembled
15
+ * fragments), client-masked input, no extensions, no fragmentation of
16
+ * outgoing messages. Development tool only: no auth, no TLS — run it
17
+ * on a trusted network. The default host (0.0.0.0) exposes the relay
18
+ * to your local network so a phone or another machine can reach it;
19
+ * pass `--host 127.0.0.1` to bind loopback-only.
20
+ */
21
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
22
+ const MAX_PAYLOAD = 16 * 1024 * 1024;
23
+ function startRelay(port, host) {
24
+ const clients = /* @__PURE__ */ new Set();
25
+ const server = createServer((_req, res) => {
26
+ res.writeHead(200, { "content-type": "text/plain" });
27
+ res.end("flatland-devtools-relay\n");
28
+ });
29
+ server.on("upgrade", (req, socket) => {
30
+ const key = req.headers["sec-websocket-key"];
31
+ if (typeof key !== "string") {
32
+ socket.destroy();
33
+ return;
34
+ }
35
+ if (req.headers["sec-websocket-version"] !== "13") {
36
+ socket.end("HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n\r\n");
37
+ return;
38
+ }
39
+ const accept = createHash("sha1").update(key + WS_GUID).digest("base64");
40
+ socket.write(`HTTP/1.1 101 Switching Protocols\r
41
+ Upgrade: websocket\r
42
+ Connection: Upgrade\r
43
+ Sec-WebSocket-Accept: ${accept}\r\n\r\n`);
44
+ const client = {
45
+ socket,
46
+ buffer: Buffer.alloc(0),
47
+ fragmentOpcode: 0,
48
+ fragments: [],
49
+ fragmentSize: 0
50
+ };
51
+ clients.add(client);
52
+ const broadcast = (payload, opcode) => {
53
+ const out = buildFrame(opcode, payload);
54
+ for (const other of clients) if (other !== client && other.socket.writable) try {
55
+ other.socket.write(out);
56
+ } catch {}
57
+ };
58
+ socket.on("data", (chunk) => {
59
+ client.buffer = Buffer.concat([client.buffer, chunk]);
60
+ let frame = readFrame(client);
61
+ while (frame !== null) {
62
+ if (frame.opcode === 8) {
63
+ socket.write(buildFrame(8, frame.payload));
64
+ socket.end();
65
+ break;
66
+ } else if (frame.opcode === 9) socket.write(buildFrame(10, frame.payload));
67
+ else if (frame.opcode === 10) {} else if (frame.opcode === 2 || frame.opcode === 1) {
68
+ if (client.fragments.length > 0) {
69
+ socket.destroy();
70
+ break;
71
+ }
72
+ if (frame.fin) broadcast(frame.payload, frame.opcode);
73
+ else {
74
+ client.fragmentOpcode = frame.opcode;
75
+ client.fragments = [frame.payload];
76
+ client.fragmentSize = frame.payload.length;
77
+ }
78
+ } else if (frame.opcode === 0) {
79
+ if (client.fragments.length === 0) {
80
+ socket.destroy();
81
+ break;
82
+ }
83
+ if (client.fragmentSize + frame.payload.length > MAX_PAYLOAD) {
84
+ socket.destroy();
85
+ break;
86
+ }
87
+ client.fragments.push(frame.payload);
88
+ client.fragmentSize += frame.payload.length;
89
+ if (frame.fin) {
90
+ broadcast(Buffer.concat(client.fragments), client.fragmentOpcode);
91
+ client.fragments = [];
92
+ client.fragmentSize = 0;
93
+ client.fragmentOpcode = 0;
94
+ }
95
+ }
96
+ frame = readFrame(client);
97
+ }
98
+ });
99
+ const drop = () => {
100
+ clients.delete(client);
101
+ socket.destroy();
102
+ };
103
+ socket.on("close", drop);
104
+ socket.on("error", drop);
105
+ });
106
+ server.listen(port, host, () => {
107
+ console.log(`flatland-devtools-relay listening on ws://${host}:${port}`);
108
+ if (host === "0.0.0.0") console.log(" warning: exposed to your local network; pass --host 127.0.0.1 for local-only");
109
+ console.log(" game: createDevtoolsProvider({ remote: \"ws://<this-host>:%d\" })", port);
110
+ console.log(" dashboard: connectRemoteDevtools(\"ws://<this-host>:%d\")", port);
111
+ });
112
+ return {
113
+ close: () => {
114
+ for (const client of clients) client.socket.destroy();
115
+ clients.clear();
116
+ server.close();
117
+ },
118
+ server
119
+ };
120
+ }
121
+ function readFrame(client) {
122
+ const buf = client.buffer;
123
+ if (buf.length < 2) return null;
124
+ const fin = (buf[0] & 128) !== 0;
125
+ const opcode = buf[0] & 15;
126
+ if (!((buf[1] & 128) !== 0)) {
127
+ client.socket.destroy();
128
+ return null;
129
+ }
130
+ let payloadLength = buf[1] & 127;
131
+ let offset = 2;
132
+ if (opcode >= 8 && (!fin || payloadLength >= 126)) {
133
+ client.socket.destroy();
134
+ return null;
135
+ }
136
+ if (payloadLength === 126) {
137
+ if (buf.length < 4) return null;
138
+ payloadLength = buf.readUInt16BE(2);
139
+ offset = 4;
140
+ } else if (payloadLength === 127) {
141
+ if (buf.length < 10) return null;
142
+ const big = buf.readBigUInt64BE(2);
143
+ if (big > BigInt(MAX_PAYLOAD)) {
144
+ client.socket.destroy();
145
+ return null;
146
+ }
147
+ payloadLength = Number(big);
148
+ offset = 10;
149
+ }
150
+ if (buf.length < offset + 4 + payloadLength) return null;
151
+ const maskedPayload = buf.subarray(offset + 4, offset + 4 + payloadLength);
152
+ const mask = buf.subarray(offset, offset + 4);
153
+ const payload = Buffer.allocUnsafe(payloadLength);
154
+ for (let i = 0; i < payloadLength; i++) payload[i] = maskedPayload[i] ^ mask[i % 4];
155
+ client.buffer = buf.subarray(offset + 4 + payloadLength);
156
+ return {
157
+ opcode,
158
+ fin,
159
+ payload
160
+ };
161
+ }
162
+ function buildFrame(opcode, payload) {
163
+ let header;
164
+ if (payload.length < 126) header = Buffer.from([128 | opcode, payload.length]);
165
+ else if (payload.length < 65536) {
166
+ header = Buffer.alloc(4);
167
+ header[0] = 128 | opcode;
168
+ header[1] = 126;
169
+ header.writeUInt16BE(payload.length, 2);
170
+ } else {
171
+ header = Buffer.alloc(10);
172
+ header[0] = 128 | opcode;
173
+ header[1] = 127;
174
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
175
+ }
176
+ return Buffer.concat([header, payload]);
177
+ }
178
+ if (process.argv[1]?.endsWith("relay.js") || process.argv[1]?.endsWith("relay.ts")) {
179
+ const args = process.argv.slice(2);
180
+ let port = 8123;
181
+ let host = "0.0.0.0";
182
+ for (let i = 0; i < args.length; i++) if (args[i] === "--port") port = Number(args[++i]);
183
+ else if (args[i] === "--host") host = String(args[++i]);
184
+ startRelay(port, host);
185
+ }
186
+ //#endregion
187
+ export { startRelay };
188
+
189
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","names":[],"sources":["../src/relay.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * flatland-devtools-relay — minimal WebSocket relay for remote/mobile\n * debugging (#114). Every frame received from one client is forwarded\n * to every other client; the provider bridge and the dashboard bridge\n * sort out semantics via the bus-frame direction sets.\n *\n * npx flatland-devtools-relay [--port 8123] [--host 0.0.0.0]\n *\n * Zero dependencies: a hand-rolled RFC 6455 server covering exactly\n * what the bridges use — frames ≤ 16 MB (single frame or reassembled\n * fragments), client-masked input, no extensions, no fragmentation of\n * outgoing messages. Development tool only: no auth, no TLS — run it\n * on a trusted network. The default host (0.0.0.0) exposes the relay\n * to your local network so a phone or another machine can reach it;\n * pass `--host 127.0.0.1` to bind loopback-only.\n */\nimport { createServer, type Server } from 'node:http'\nimport { createHash } from 'node:crypto'\nimport type { Duplex } from 'node:stream'\n\nconst WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'\nconst MAX_PAYLOAD = 16 * 1024 * 1024\n\ninterface Client {\n socket: Duplex\n buffer: Buffer\n /** In-flight fragmented message: first-frame opcode + accumulated payload. */\n fragmentOpcode: number\n fragments: Buffer[]\n /** Running total of `fragments` payload bytes — bounds reassembly to MAX_PAYLOAD. */\n fragmentSize: number\n}\n\nexport interface RelayHandle {\n /** Stop the relay: destroys all client sockets and closes the server. */\n close: () => void\n /**\n * The underlying HTTP server — use `.address()` to read the bound\n * port when `port` was passed as 0 (ephemeral; tests only, the CLI\n * always passes an explicit port).\n */\n server: Server\n}\n\nexport function startRelay(port: number, host: string): RelayHandle {\n const clients = new Set<Client>()\n\n const server = createServer((_req, res) => {\n res.writeHead(200, { 'content-type': 'text/plain' })\n res.end('flatland-devtools-relay\\n')\n })\n\n server.on('upgrade', (req, socket) => {\n const key = req.headers['sec-websocket-key']\n if (typeof key !== 'string') {\n socket.destroy()\n return\n }\n if (req.headers['sec-websocket-version'] !== '13') {\n // `end(data)` writes then closes only once the write has flushed —\n // an immediate `write()` + `destroy()` pair can drop the response\n // under backpressure before the peer ever sees it.\n socket.end('HTTP/1.1 426 Upgrade Required\\r\\nSec-WebSocket-Version: 13\\r\\n\\r\\n')\n return\n }\n const accept = createHash('sha1')\n .update(key + WS_GUID)\n .digest('base64')\n socket.write(\n 'HTTP/1.1 101 Switching Protocols\\r\\n' +\n 'Upgrade: websocket\\r\\n' +\n 'Connection: Upgrade\\r\\n' +\n `Sec-WebSocket-Accept: ${accept}\\r\\n\\r\\n`\n )\n\n const client: Client = {\n socket,\n buffer: Buffer.alloc(0),\n fragmentOpcode: 0,\n fragments: [],\n fragmentSize: 0,\n }\n clients.add(client)\n\n const broadcast = (payload: Buffer, opcode: number): void => {\n const out = buildFrame(opcode, payload)\n for (const other of clients) {\n if (other !== client && other.socket.writable) {\n try {\n other.socket.write(out)\n } catch {\n // Peer closed mid-write — drop the frame, its own close/error\n // handler will evict it from `clients`.\n }\n }\n }\n }\n\n socket.on('data', (chunk: Buffer) => {\n client.buffer = Buffer.concat([client.buffer, chunk])\n let frame = readFrame(client)\n while (frame !== null) {\n if (frame.opcode === 0x8) {\n // close — echo the close frame per RFC 6455 §5.5.1, then end.\n socket.write(buildFrame(0x8, frame.payload))\n socket.end()\n break\n } else if (frame.opcode === 0x9) {\n // ping → pong (control frames may interleave with fragments)\n socket.write(buildFrame(0xa, frame.payload))\n } else if (frame.opcode === 0xa) {\n // pong — ignore\n } else if (frame.opcode === 0x2 || frame.opcode === 0x1) {\n if (client.fragments.length > 0) {\n // A new data frame while a fragmented message is still open —\n // only control frames may interleave fragments (RFC §5.4).\n socket.destroy()\n break\n }\n if (frame.fin) {\n broadcast(frame.payload, frame.opcode)\n } else {\n // Start of a fragmented message.\n client.fragmentOpcode = frame.opcode\n client.fragments = [frame.payload]\n client.fragmentSize = frame.payload.length\n }\n } else if (frame.opcode === 0x0) {\n // Continuation of a fragmented message.\n if (client.fragments.length === 0) {\n socket.destroy() // continuation with no start — protocol error\n break\n }\n // Each frame is already capped at MAX_PAYLOAD by readFrame, but\n // an attacker can drip-feed unbounded small continuations —\n // bound the reassembled total too.\n if (client.fragmentSize + frame.payload.length > MAX_PAYLOAD) {\n socket.destroy()\n break\n }\n client.fragments.push(frame.payload)\n client.fragmentSize += frame.payload.length\n if (frame.fin) {\n broadcast(Buffer.concat(client.fragments), client.fragmentOpcode)\n client.fragments = []\n client.fragmentSize = 0\n client.fragmentOpcode = 0\n }\n }\n frame = readFrame(client)\n }\n })\n\n const drop = (): void => {\n clients.delete(client)\n socket.destroy()\n }\n socket.on('close', drop)\n socket.on('error', drop)\n })\n\n server.listen(port, host, () => {\n console.log(`flatland-devtools-relay listening on ws://${host}:${port}`)\n if (host === '0.0.0.0') {\n console.log(' warning: exposed to your local network; pass --host 127.0.0.1 for local-only')\n }\n console.log(' game: createDevtoolsProvider({ remote: \"ws://<this-host>:%d\" })', port)\n console.log(' dashboard: connectRemoteDevtools(\"ws://<this-host>:%d\")', port)\n })\n\n return {\n close: () => {\n for (const client of clients) client.socket.destroy()\n clients.clear()\n server.close()\n },\n server,\n }\n}\n\ninterface WsFrame {\n opcode: number\n fin: boolean\n payload: Buffer\n}\n\nfunction readFrame(client: Client): WsFrame | null {\n const buf = client.buffer\n if (buf.length < 2) return null\n const fin = (buf[0]! & 0x80) !== 0\n const opcode = buf[0]! & 0x0f\n const masked = (buf[1]! & 0x80) !== 0\n if (!masked) {\n // RFC 6455 §5.1: the server MUST fail the connection if a client\n // frame arrives unmasked.\n client.socket.destroy()\n return null\n }\n let payloadLength = buf[1]! & 0x7f\n let offset = 2\n // Control frames (close/ping/pong, opcode >= 0x8) must not be\n // fragmented and their payload must fit the 7-bit length field\n // directly — RFC §5.5 caps them at 125 bytes and forbids the\n // 126/127 extended encodings entirely, regardless of the real length.\n if (opcode >= 0x8 && (!fin || payloadLength >= 126)) {\n client.socket.destroy()\n return null\n }\n if (payloadLength === 126) {\n if (buf.length < 4) return null\n payloadLength = buf.readUInt16BE(2)\n offset = 4\n } else if (payloadLength === 127) {\n if (buf.length < 10) return null\n const big = buf.readBigUInt64BE(2)\n if (big > BigInt(MAX_PAYLOAD)) {\n client.socket.destroy()\n return null\n }\n payloadLength = Number(big)\n offset = 10\n }\n if (buf.length < offset + 4 + payloadLength) return null\n\n const maskedPayload = buf.subarray(offset + 4, offset + 4 + payloadLength)\n const mask = buf.subarray(offset, offset + 4)\n const payload = Buffer.allocUnsafe(payloadLength)\n for (let i = 0; i < payloadLength; i++) {\n payload[i] = maskedPayload[i]! ^ mask[i % 4]!\n }\n\n client.buffer = buf.subarray(offset + 4 + payloadLength)\n return { opcode, fin, payload }\n}\n\nfunction buildFrame(opcode: number, payload: Buffer): Buffer {\n let header: Buffer\n if (payload.length < 126) {\n header = Buffer.from([0x80 | opcode, payload.length])\n } else if (payload.length < 0x10000) {\n header = Buffer.alloc(4)\n header[0] = 0x80 | opcode\n header[1] = 126\n header.writeUInt16BE(payload.length, 2)\n } else {\n header = Buffer.alloc(10)\n header[0] = 0x80 | opcode\n header[1] = 127\n header.writeBigUInt64BE(BigInt(payload.length), 2)\n }\n return Buffer.concat([header, payload])\n}\n\n// Invoked as a bin\nconst isMain = process.argv[1]?.endsWith('relay.js') || process.argv[1]?.endsWith('relay.ts')\nif (isMain) {\n const args = process.argv.slice(2)\n let port = 8123\n let host = '0.0.0.0'\n for (let i = 0; i < args.length; i++) {\n if (args[i] === '--port') port = Number(args[++i])\n else if (args[i] === '--host') host = String(args[++i])\n }\n startRelay(port, host)\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { RemoteBridgeHandle, WebSocketLike } from "three-flatland";
2
+ //#region src/remote.d.ts
3
+ interface ConnectRemoteDevtoolsOptions {
4
+ /** Override the discovery channel (matches the provider's override). */
5
+ discoveryChannelName?: string;
6
+ }
7
+ declare function connectRemoteDevtools(remote: string | WebSocketLike, options?: ConnectRemoteDevtoolsOptions): RemoteBridgeHandle;
8
+ //#endregion
9
+ export { ConnectRemoteDevtoolsOptions, connectRemoteDevtools };
10
+ //# sourceMappingURL=remote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.d.ts","names":[],"sources":["../src/remote.ts"],"mappings":";;UAkBiB;;EAEf;;iBAGc,sBACd,iBAAiB,eACjB,UAAS,+BACR"}
package/dist/remote.js ADDED
@@ -0,0 +1,28 @@
1
+ import { DISCOVERY_CHANNEL } from "three-flatland/debug-protocol";
2
+ import { createConsumerRemoteBridge } from "three-flatland";
3
+ //#region src/remote.ts
4
+ /**
5
+ * Remote-debugging attach for the dashboard side (#114).
6
+ *
7
+ * Connect the dashboard's page to a `flatland-devtools-relay` (or any
8
+ * WS endpoint a remote game's provider bridge is connected to) and the
9
+ * remote provider appears on the local bus exactly like an in-page one
10
+ * — discovery, subscribe flow, stats, buffers, all unchanged.
11
+ *
12
+ * ```ts
13
+ * import { connectRemoteDevtools } from '@three-flatland/devtools'
14
+ * const remote = connectRemoteDevtools('ws://192.168.1.20:8123')
15
+ * // … later
16
+ * remote.dispose()
17
+ * ```
18
+ */
19
+ function connectRemoteDevtools(remote, options = {}) {
20
+ return createConsumerRemoteBridge({
21
+ remote,
22
+ discoveryChannelName: options.discoveryChannelName ?? DISCOVERY_CHANNEL
23
+ });
24
+ }
25
+ //#endregion
26
+ export { connectRemoteDevtools };
27
+
28
+ //# sourceMappingURL=remote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.js","names":[],"sources":["../src/remote.ts"],"sourcesContent":["/**\n * Remote-debugging attach for the dashboard side (#114).\n *\n * Connect the dashboard's page to a `flatland-devtools-relay` (or any\n * WS endpoint a remote game's provider bridge is connected to) and the\n * remote provider appears on the local bus exactly like an in-page one\n * — discovery, subscribe flow, stats, buffers, all unchanged.\n *\n * ```ts\n * import { connectRemoteDevtools } from '@three-flatland/devtools'\n * const remote = connectRemoteDevtools('ws://192.168.1.20:8123')\n * // … later\n * remote.dispose()\n * ```\n */\nimport { createConsumerRemoteBridge, type RemoteBridgeHandle, type WebSocketLike } from 'three-flatland'\nimport { DISCOVERY_CHANNEL } from 'three-flatland/debug-protocol'\n\nexport interface ConnectRemoteDevtoolsOptions {\n /** Override the discovery channel (matches the provider's override). */\n discoveryChannelName?: string\n}\n\nexport function connectRemoteDevtools(\n remote: string | WebSocketLike,\n options: ConnectRemoteDevtoolsOptions = {}\n): RemoteBridgeHandle {\n return createConsumerRemoteBridge({\n remote,\n discoveryChannelName: options.discoveryChannelName ?? DISCOVERY_CHANNEL,\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuBA,SAAgB,sBACd,QACA,UAAwC,CAAC,GACrB;CACpB,OAAO,2BAA2B;EAChC;EACA,sBAAsB,QAAQ,wBAAwB;CACxD,CAAC;AACH"}
@@ -1,40 +1,32 @@
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
-
1
+ import { DevtoolsClient } from "./devtools-client.js";
2
+ import { FolderApi, Pane } from "tweakpane";
3
+ //#region src/stats-graph.d.ts
13
4
  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;
5
+ /** The root element */
6
+ readonly element: HTMLElement;
7
+ /**
8
+ * Render a frame. Required when constructed with `driver: 'manual'`;
9
+ * no-op for the `'raf'` driver (the internal rAF is doing it).
10
+ */
11
+ update(): void;
12
+ /** Stop and remove. */
13
+ dispose(): void;
23
14
  }
24
15
  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';
16
+ rows?: number;
17
+ /**
18
+ * `'raf'` (default) — the graph runs its own `requestAnimationFrame`
19
+ * loop. `'manual'` — no rAF; caller drives via `handle.update()` from
20
+ * their own frame tick (e.g. `renderer.setAnimationLoop`, R3F
21
+ * `useFrame`).
22
+ */
23
+ driver?: 'raf' | 'manual';
33
24
  }
34
25
  /**
35
26
  * Add a cycling stats graph to a Tweakpane parent.
36
27
  * Matches the visual design of the built-in fpsgraph blade.
37
28
  */
38
29
  declare function addStatsGraph(parent: Pane | FolderApi, client: DevtoolsClient, options?: AddStatsGraphOptions): StatsGraphHandle;
39
-
40
- export { type AddStatsGraphOptions, type StatsGraphHandle, addStatsGraph };
30
+ //#endregion
31
+ export { AddStatsGraphOptions, StatsGraphHandle, addStatsGraph };
32
+ //# sourceMappingURL=stats-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats-graph.d.ts","names":[],"sources":["../src/stats-graph.ts"],"mappings":";;;UAaiB;;WAEN,SAAS;;;;;EAKlB;;EAEA;;UAGe;EACf;;;;;;;EAOA;;;;;;iBAgCc,cACd,QAAQ,OAAO,WACf,QAAQ,gBACR,UAAS,uBACR"}