@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 @@
1
+ {"version":3,"sources":["../src/buffers-view.ts"],"sourcesContent":["/**\n * Buffers view — compact one-row preview of the currently-selected\n * debug buffer, with `◀ name ▶` arrows to cycle through every\n * registered entry and a ⤢ \"expand\" icon overlay on the thumbnail\n * that opens (a future) fullscreen modal.\n *\n * Main-pane real estate is tight: there's no grouping UI here. The\n * fullscreen modal is where the group tree / channel toggles / pan &\n * zoom live. Starts collapsed — no pixel readback happens until the\n * user opens the blade.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\n\nimport type { BufferSnapshot, DevtoolsClient } from './devtools-client.js'\n\nexport interface BuffersViewHandle {\n readonly element: HTMLElement\n /** Tell the view the modal is open/closed. While open, the view\n * defers buffer selection to the modal. */\n setModalOpen(open: boolean): void\n /** Sync the view's active buffer to match the modal's selection. */\n setActiveFromModal(name: string): void\n /** Tell the view the outer pane was folded/unfolded. When folded the\n * view unsubscribes regardless of its own collapse state. */\n setPaneFolded(folded: boolean): void\n dispose(): void\n}\n\nconst THUMB_WIDTH = 240\n// A bit taller than before now that the size/format text overlays the\n// canvas instead of taking its own row below.\nconst THUMB_HEIGHT = 120\n\nexport interface AddBuffersViewOptions {\n /**\n * Invoked when the user clicks the bottom-right ⤢ button. Wired by\n * `createPane` to open the fullscreen modal viewer with the active\n * buffer pre-selected. When omitted, the click is a no-op.\n */\n onExpand?: (name: string) => void\n}\n\nexport function addBuffersView(\n parent: Pane | FolderApi,\n client: DevtoolsClient,\n options: AddBuffersViewOptions = {},\n): BuffersViewHandle {\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 // Same darker-bg treatment as registry-view to sink debug readouts\n // visually behind the controls.\n bladeEl.style.cssText = 'display:none;flex-direction:column;background:rgba(0,0,0,0.3)'\n\n // Header — identical pattern to registry-view (click to collapse,\n // arrows cycle with stopPropagation so they don't also toggle).\n const header = document.createElement('div')\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\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 buffer')\n const nameLabel = document.createElement('span')\n nameLabel.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 buffer')\n header.appendChild(prevBtn)\n header.appendChild(nameLabel)\n header.appendChild(nextBtn)\n\n // Body — thumbnail canvas with two corner overlays: dimensions/format\n // bottom-left, expand icon bottom-right. No separate text row, so the\n // canvas itself gets the full body height.\n const body = document.createElement('div')\n body.style.cssText = 'display:none;flex-direction:column;padding:2px 6px 6px;position:relative'\n\n // Display canvas. Backing dimensions are kept in sync with the\n // actual rendered CSS box via ResizeObserver — same pattern as\n // `stats-graph.ts`. Without this, the bitmap stays at its initial\n // 240×96 intrinsic size and (depending on the parent layout) ends up\n // displayed unscaled in the upper-left of a stretched-CSS box.\n const canvas = document.createElement('canvas')\n canvas.width = THUMB_WIDTH\n canvas.height = THUMB_HEIGHT\n canvas.style.cssText = `display:block;width:100%;height:${THUMB_HEIGHT}px;background:rgba(0,2,28,0.45);border-radius:2px;image-rendering:pixelated`\n\n // Off-screen canvas, sized per source bitmap. Used to materialise\n // `putImageData` once per frame; we then `drawImage` it onto the\n // display canvas with an aspect-correct destination rect.\n const offscreen = document.createElement('canvas')\n offscreen.width = 1\n offscreen.height = 1\n\n // Bottom-left overlay: dimensions + pixel format. Same translucent\n // chip background as the FPS-graph value badge so the text stays\n // legible regardless of the buffer's content underneath.\n const valueLabel = document.createElement('span')\n valueLabel.style.cssText = [\n 'position:absolute',\n 'left:10px',\n 'bottom:10px',\n 'padding:1px 4px',\n 'font-size:10px',\n 'background:rgba(0,2,28,0.65)',\n 'border-radius:2px',\n 'color:var(--tp-monitor-foreground-color)',\n 'opacity:0.85',\n 'font-family:var(--tp-base-font-family, ui-monospace, monospace)',\n 'font-variant-numeric:tabular-nums',\n 'pointer-events:none',\n ].join(';')\n\n // Bottom-right overlay: expand-to-fullscreen button.\n const expandBtn = document.createElement('span')\n expandBtn.textContent = '⤢'\n expandBtn.setAttribute('role', 'button')\n expandBtn.setAttribute('aria-label', 'Open fullscreen buffer viewer')\n expandBtn.title = 'Open fullscreen buffer viewer'\n expandBtn.style.cssText = [\n 'position:absolute',\n 'right:10px',\n 'bottom:10px',\n 'padding:1px 4px',\n 'font-size:11px',\n 'background:rgba(0,2,28,0.65)',\n 'border-radius:2px',\n 'cursor:pointer',\n 'color:var(--tp-monitor-foreground-color)',\n 'user-select:none',\n '-webkit-user-select:none',\n ].join(';')\n\n body.appendChild(canvas)\n body.appendChild(valueLabel)\n body.appendChild(expandBtn)\n\n bladeEl.appendChild(header)\n bladeEl.appendChild(body)\n\n // ── State ─────────────────────────────────────────────────────────────\n\n let collapsed = true\n let paneFolded = false\n let activeName: string | null = null\n let lastRenderedVersion = -1\n const ctx = canvas.getContext('2d')\n\n // Cached ImageData reused across paints when dimensions match — without\n // this, `createImageData(w, h)` allocates a fresh Uint8ClampedArray\n // (~w*h*4 bytes) every frame, ~400 KB/s of throwaway garbage at 4 Hz\n // for a 256×96 thumb.\n let cachedImgData: ImageData | null = null\n let cachedImgDataW = 0\n let cachedImgDataH = 0\n\n // Keep the canvas backing locked to its actual rendered pixel size so\n // `drawImage(0,0,canvas.width,canvas.height)` truly fills what the\n // user sees. Re-render the active buffer on resize so a freshly-\n // expanded blade isn't stuck on a stale stretched bitmap.\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return\n const dpr = window.devicePixelRatio || 1\n const cssW = Math.max(1, Math.round(entry.contentRect.width))\n const cssH = Math.max(1, Math.round(entry.contentRect.height))\n const w = Math.max(1, Math.round(cssW * dpr))\n const h = Math.max(1, Math.round(cssH * dpr))\n if (canvas.width !== w || canvas.height !== h) {\n canvas.width = w\n canvas.height = h\n lastRenderedVersion = -1 // force a redraw at the new backing size\n const cur = activeName !== null ? client.state.buffers.get(activeName) : undefined\n if (cur && !collapsed) renderThumb(cur)\n }\n })\n resizeObserver.observe(canvas)\n\n function currentNames(): string[] {\n return Array.from(client.state.buffers.keys())\n }\n\n let modalOpen = false\n\n function syncSelection(): void {\n if (modalOpen) return\n if (paneFolded || collapsed || activeName === null) {\n // Fully idle — provider skips readbacks entirely.\n client.setBuffers({})\n return\n }\n // Thumbnail panel is visible → ask for a small downsampled preview.\n client.setBuffers({ [activeName]: { mode: 'thumbnail', thumbSize: 256 } })\n }\n\n function setActive(name: string | null): void {\n if (activeName === name) return\n activeName = name\n nameLabel.textContent = name ?? '—'\n lastRenderedVersion = -1\n syncSelection()\n }\n\n function cycle(delta: number): void {\n const names = currentNames()\n if (names.length === 0) return\n const idx = activeName === null ? -1 : names.indexOf(activeName)\n const next = names[(idx + delta + names.length) % names.length]!\n setActive(next)\n }\n\n const toggleCollapse = (e: Event): void => {\n e.stopPropagation()\n collapsed = !collapsed\n body.style.display = collapsed ? 'none' : 'flex'\n if (!collapsed) lastRenderedVersion = -1\n syncSelection()\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 expandBtn.addEventListener('click', (e) => {\n e.stopPropagation()\n if (activeName !== null && options.onExpand !== undefined) {\n options.onExpand(activeName)\n }\n })\n\n // ── Render ────────────────────────────────────────────────────────────\n\n /**\n * Paint the latest sample for the currently-selected buffer into the\n * thumbnail. Float kinds are clamped to [0,1] for the preview;\n * proper min-max tonemap lives in the fullscreen viewer (TBD).\n *\n * Pipeline:\n * 1. Decode the typed-array sample into RGBA8 inside `offscreen`\n * (sized to match the source — 256×4, 64×64, etc.).\n * 2. Clear the display canvas.\n * 3. Compute an aspect-correct destination rect inside the display\n * canvas (THUMB_WIDTH × THUMB_HEIGHT) and `drawImage` into it.\n *\n * Disabling `imageSmoothingEnabled` keeps the upscale crisp instead\n * of bilinear-blurry — matches the \"image-rendering: pixelated\" hint.\n */\n function renderThumb(entry: BufferSnapshot): void {\n if (!ctx) return\n ctx.clearRect(0, 0, canvas.width, canvas.height)\n\n const { width, height, pixels } = entry\n if (width === 0 || height === 0 || pixels === null) return\n\n if (offscreen.width !== width || offscreen.height !== height) {\n offscreen.width = width\n offscreen.height = height\n // Backing dims changed → invalidate the ImageData cache.\n cachedImgData = null\n }\n const offCtx = offscreen.getContext('2d')\n if (!offCtx) return\n\n if (cachedImgData === null || cachedImgDataW !== width || cachedImgDataH !== height) {\n cachedImgData = offCtx.createImageData(width, height)\n cachedImgDataW = width\n cachedImgDataH = height\n }\n const imgData = cachedImgData\n const out = imgData.data\n\n // Pixels arrive as display-ready RGBA8 from the worker — copy\n // directly into the ImageData without any decoder logic.\n if (pixels instanceof Uint8Array) {\n out.set(pixels.subarray(0, out.length))\n } else {\n // Float32Array fallback (shouldn't happen post-pipeline, but\n // guard against legacy callers).\n const count = width * height\n for (let i = 0; i < count; i++) {\n const o = i * 4\n out[o] = Math.round(Math.max(0, Math.min(1, pixels[i * 4] ?? 0)) * 255)\n out[o + 1] = Math.round(Math.max(0, Math.min(1, pixels[i * 4 + 1] ?? 0)) * 255)\n out[o + 2] = Math.round(Math.max(0, Math.min(1, pixels[i * 4 + 2] ?? 0)) * 255)\n out[o + 3] = Math.round(Math.max(0, Math.min(1, pixels[i * 4 + 3] ?? 1)) * 255)\n }\n }\n\n offCtx.putImageData(imgData, 0, 0)\n\n // Stretch to fill: every source pixel maps somewhere in the\n // viewport. This deliberately distorts (a 256×4 buffer reads as\n // 4 stretched rows across the full panel) but guarantees you can\n // SEE every value in a debug thumbnail. Aspect-correct inspection\n // belongs in the fullscreen viewer.\n ctx.imageSmoothingEnabled = false\n ctx.drawImage(offscreen, 0, 0, canvas.width, canvas.height)\n }\n\n // ── Listener ──────────────────────────────────────────────────────────\n\n const unsubscribe = client.addListener((state) => {\n const names = Array.from(state.buffers.keys())\n\n // Visibility.\n bladeEl.style.display = names.length > 0 ? 'flex' : 'none'\n\n // Establish/repair active selection.\n if (names.length === 0) {\n setActive(null)\n } else if (activeName === null || !names.includes(activeName)) {\n setActive(names[0]!)\n }\n\n // Arrow affordance.\n const multi = names.length > 1\n prevBtn.style.opacity = multi ? '0.8' : '0.25'\n nextBtn.style.opacity = multi ? '0.8' : '0.25'\n prevBtn.style.cursor = multi ? 'pointer' : 'default'\n nextBtn.style.cursor = multi ? 'pointer' : 'default'\n\n // Ensure the provider is streaming the current selection (new\n // entries appearing while we're expanded should auto-subscribe).\n syncSelection()\n\n // Paint.\n if (collapsed || activeName === null) return\n const entry = state.buffers.get(activeName)\n if (!entry) return\n if (entry.version === lastRenderedVersion) return\n lastRenderedVersion = entry.version\n renderThumb(entry)\n // Prefer source dims for the label — the shipped thumbnail dims\n // (e.g. 256×16) confuse users who want to know the real buffer size.\n const lw = entry.srcWidth > 0 ? entry.srcWidth : entry.width\n const lh = entry.srcHeight > 0 ? entry.srcHeight : entry.height\n valueLabel.textContent = `${lw}×${lh} · ${entry.pixelType}`\n nameLabel.title = `${entry.name}${entry.label ? ` — ${entry.label}` : ''}`\n })\n\n return {\n element: bladeEl,\n setModalOpen(open: boolean) {\n modalOpen = open\n if (!open) syncSelection()\n },\n setActiveFromModal(name: string) {\n if (activeName === name) return\n activeName = name\n nameLabel.textContent = name\n lastRenderedVersion = -1\n },\n setPaneFolded(folded: boolean) {\n if (paneFolded === folded) return\n paneFolded = folded\n if (folded) lastRenderedVersion = -1\n syncSelection()\n },\n dispose() {\n resizeObserver.disconnect()\n unsubscribe()\n blade.dispose()\n },\n }\n}\n\n"],"mappings":"AA6BA,MAAM,cAAc;AAGpB,MAAM,eAAe;AAWd,SAAS,eACd,QACA,QACA,UAAiC,CAAC,GACf;AACnB,QAAM,QAAQ,OAAO,SAAS,EAAE,MAAM,YAAY,CAAC;AAInD,QAAM,UAAU,MAAM;AACtB,UAAQ,YAAY;AACpB,UAAQ,YAAY;AAGpB,UAAQ,MAAM,UAAU;AAIxB,QAAM,SAAS,SAAS,cAAc,KAAK;AAC3C,SAAO,MAAM,UAAU;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,GAAG;AAEV,QAAM,aAAa;AACnB,QAAM,UAAU,SAAS,cAAc,MAAM;AAC7C,UAAQ,cAAc;AACtB,UAAQ,MAAM,UAAU;AACxB,UAAQ,aAAa,QAAQ,QAAQ;AACrC,UAAQ,aAAa,cAAc,iBAAiB;AACpD,QAAM,YAAY,SAAS,cAAc,MAAM;AAC/C,YAAU,MAAM,UAAU;AAC1B,QAAM,UAAU,SAAS,cAAc,MAAM;AAC7C,UAAQ,cAAc;AACtB,UAAQ,MAAM,UAAU;AACxB,UAAQ,aAAa,QAAQ,QAAQ;AACrC,UAAQ,aAAa,cAAc,aAAa;AAChD,SAAO,YAAY,OAAO;AAC1B,SAAO,YAAY,SAAS;AAC5B,SAAO,YAAY,OAAO;AAK1B,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,MAAM,UAAU;AAOrB,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ;AACf,SAAO,SAAS;AAChB,SAAO,MAAM,UAAU,mCAAmC,YAAY;AAKtE,QAAM,YAAY,SAAS,cAAc,QAAQ;AACjD,YAAU,QAAQ;AAClB,YAAU,SAAS;AAKnB,QAAM,aAAa,SAAS,cAAc,MAAM;AAChD,aAAW,MAAM,UAAU;AAAA,IACzB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,GAAG;AAGV,QAAM,YAAY,SAAS,cAAc,MAAM;AAC/C,YAAU,cAAc;AACxB,YAAU,aAAa,QAAQ,QAAQ;AACvC,YAAU,aAAa,cAAc,+BAA+B;AACpE,YAAU,QAAQ;AAClB,YAAU,MAAM,UAAU;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,GAAG;AAEV,OAAK,YAAY,MAAM;AACvB,OAAK,YAAY,UAAU;AAC3B,OAAK,YAAY,SAAS;AAE1B,UAAQ,YAAY,MAAM;AAC1B,UAAQ,YAAY,IAAI;AAIxB,MAAI,YAAY;AAChB,MAAI,aAAa;AACjB,MAAI,aAA4B;AAChC,MAAI,sBAAsB;AAC1B,QAAM,MAAM,OAAO,WAAW,IAAI;AAMlC,MAAI,gBAAkC;AACtC,MAAI,iBAAiB;AACrB,MAAI,iBAAiB;AAMrB,QAAM,iBAAiB,IAAI,eAAe,CAAC,CAAC,KAAK,MAAM;AACrD,QAAI,CAAC,MAAO;AACZ,UAAM,MAAM,OAAO,oBAAoB;AACvC,UAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,YAAY,KAAK,CAAC;AAC5D,UAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,YAAY,MAAM,CAAC;AAC7D,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,GAAG,CAAC;AAC5C,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,GAAG,CAAC;AAC5C,QAAI,OAAO,UAAU,KAAK,OAAO,WAAW,GAAG;AAC7C,aAAO,QAAQ;AACf,aAAO,SAAS;AAChB,4BAAsB;AACtB,YAAM,MAAM,eAAe,OAAO,OAAO,MAAM,QAAQ,IAAI,UAAU,IAAI;AACzE,UAAI,OAAO,CAAC,UAAW,aAAY,GAAG;AAAA,IACxC;AAAA,EACF,CAAC;AACD,iBAAe,QAAQ,MAAM;AAE7B,WAAS,eAAyB;AAChC,WAAO,MAAM,KAAK,OAAO,MAAM,QAAQ,KAAK,CAAC;AAAA,EAC/C;AAEA,MAAI,YAAY;AAEhB,WAAS,gBAAsB;AAC7B,QAAI,UAAW;AACf,QAAI,cAAc,aAAa,eAAe,MAAM;AAElD,aAAO,WAAW,CAAC,CAAC;AACpB;AAAA,IACF;AAEA,WAAO,WAAW,EAAE,CAAC,UAAU,GAAG,EAAE,MAAM,aAAa,WAAW,IAAI,EAAE,CAAC;AAAA,EAC3E;AAEA,WAAS,UAAU,MAA2B;AAC5C,QAAI,eAAe,KAAM;AACzB,iBAAa;AACb,cAAU,cAAc,QAAQ;AAChC,0BAAsB;AACtB,kBAAc;AAAA,EAChB;AAEA,WAAS,MAAM,OAAqB;AAClC,UAAM,QAAQ,aAAa;AAC3B,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,MAAM,eAAe,OAAO,KAAK,MAAM,QAAQ,UAAU;AAC/D,UAAM,OAAO,OAAO,MAAM,QAAQ,MAAM,UAAU,MAAM,MAAM;AAC9D,cAAU,IAAI;AAAA,EAChB;AAEA,QAAM,iBAAiB,CAAC,MAAmB;AACzC,MAAE,gBAAgB;AAClB,gBAAY,CAAC;AACb,SAAK,MAAM,UAAU,YAAY,SAAS;AAC1C,QAAI,CAAC,UAAW,uBAAsB;AACtC,kBAAc;AAAA,EAChB;AACA,SAAO,iBAAiB,SAAS,cAAc;AAC/C,UAAQ,iBAAiB,SAAS,CAAC,MAAM;AAAE,MAAE,gBAAgB;AAAG,UAAM,EAAE;AAAA,EAAE,CAAC;AAC3E,UAAQ,iBAAiB,SAAS,CAAC,MAAM;AAAE,MAAE,gBAAgB;AAAG,UAAM,CAAC;AAAA,EAAE,CAAC;AAC1E,YAAU,iBAAiB,SAAS,CAAC,MAAM;AACzC,MAAE,gBAAgB;AAClB,QAAI,eAAe,QAAQ,QAAQ,aAAa,QAAW;AACzD,cAAQ,SAAS,UAAU;AAAA,IAC7B;AAAA,EACF,CAAC;AAmBD,WAAS,YAAY,OAA6B;AAChD,QAAI,CAAC,IAAK;AACV,QAAI,UAAU,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAE/C,UAAM,EAAE,OAAO,QAAQ,OAAO,IAAI;AAClC,QAAI,UAAU,KAAK,WAAW,KAAK,WAAW,KAAM;AAEpD,QAAI,UAAU,UAAU,SAAS,UAAU,WAAW,QAAQ;AAC5D,gBAAU,QAAQ;AAClB,gBAAU,SAAS;AAEnB,sBAAgB;AAAA,IAClB;AACA,UAAM,SAAS,UAAU,WAAW,IAAI;AACxC,QAAI,CAAC,OAAQ;AAEb,QAAI,kBAAkB,QAAQ,mBAAmB,SAAS,mBAAmB,QAAQ;AACnF,sBAAgB,OAAO,gBAAgB,OAAO,MAAM;AACpD,uBAAiB;AACjB,uBAAiB;AAAA,IACnB;AACA,UAAM,UAAU;AAChB,UAAM,MAAM,QAAQ;AAIpB,QAAI,kBAAkB,YAAY;AAChC,UAAI,IAAI,OAAO,SAAS,GAAG,IAAI,MAAM,CAAC;AAAA,IACxC,OAAO;AAGL,YAAM,QAAQ,QAAQ;AACtB,eAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,cAAM,IAAI,IAAI;AACd,YAAI,CAAC,IAAQ,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,KAAS,CAAC,CAAC,IAAI,GAAG;AAC9E,YAAI,IAAI,CAAC,IAAI,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG;AAC9E,YAAI,IAAI,CAAC,IAAI,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG;AAC9E,YAAI,IAAI,CAAC,IAAI,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG;AAAA,MAChF;AAAA,IACF;AAEA,WAAO,aAAa,SAAS,GAAG,CAAC;AAOjC,QAAI,wBAAwB;AAC5B,QAAI,UAAU,WAAW,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAAA,EAC5D;AAIA,QAAM,cAAc,OAAO,YAAY,CAAC,UAAU;AAChD,UAAM,QAAQ,MAAM,KAAK,MAAM,QAAQ,KAAK,CAAC;AAG7C,YAAQ,MAAM,UAAU,MAAM,SAAS,IAAI,SAAS;AAGpD,QAAI,MAAM,WAAW,GAAG;AACtB,gBAAU,IAAI;AAAA,IAChB,WAAW,eAAe,QAAQ,CAAC,MAAM,SAAS,UAAU,GAAG;AAC7D,gBAAU,MAAM,CAAC,CAAE;AAAA,IACrB;AAGA,UAAM,QAAQ,MAAM,SAAS;AAC7B,YAAQ,MAAM,UAAU,QAAQ,QAAQ;AACxC,YAAQ,MAAM,UAAU,QAAQ,QAAQ;AACxC,YAAQ,MAAM,SAAS,QAAQ,YAAY;AAC3C,YAAQ,MAAM,SAAS,QAAQ,YAAY;AAI3C,kBAAc;AAGd,QAAI,aAAa,eAAe,KAAM;AACtC,UAAM,QAAQ,MAAM,QAAQ,IAAI,UAAU;AAC1C,QAAI,CAAC,MAAO;AACZ,QAAI,MAAM,YAAY,oBAAqB;AAC3C,0BAAsB,MAAM;AAC5B,gBAAY,KAAK;AAGjB,UAAM,KAAK,MAAM,WAAW,IAAI,MAAM,WAAW,MAAM;AACvD,UAAM,KAAK,MAAM,YAAY,IAAI,MAAM,YAAY,MAAM;AACzD,eAAW,cAAc,GAAG,EAAE,OAAI,EAAE,SAAM,MAAM,SAAS;AACzD,cAAU,QAAQ,GAAG,MAAM,IAAI,GAAG,MAAM,QAAQ,WAAM,MAAM,KAAK,KAAK,EAAE;AAAA,EAC1E,CAAC;AAED,SAAO;AAAA,IACL,SAAS;AAAA,IACT,aAAa,MAAe;AAC1B,kBAAY;AACZ,UAAI,CAAC,KAAM,eAAc;AAAA,IAC3B;AAAA,IACA,mBAAmB,MAAc;AAC/B,UAAI,eAAe,KAAM;AACzB,mBAAa;AACb,gBAAU,cAAc;AACxB,4BAAsB;AAAA,IACxB;AAAA,IACA,cAAc,QAAiB;AAC7B,UAAI,eAAe,OAAQ;AAC3B,mBAAa;AACb,UAAI,OAAQ,uBAAsB;AAClC,oBAAc;AAAA,IAChB;AAAA,IACA,UAAU;AACR,qBAAe,WAAW;AAC1B,kBAAY;AACZ,YAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,203 @@
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 create_pane_exports = {};
20
+ __export(create_pane_exports, {
21
+ claimPane: () => claimPane,
22
+ createPane: () => createPane
23
+ });
24
+ module.exports = __toCommonJS(create_pane_exports);
25
+ var import_tweakpane = require("tweakpane");
26
+ var import_theme = require("./theme.js");
27
+ var import_plugins = require("./plugins.js");
28
+ var import_stats_graph = require("./stats-graph.js");
29
+ var import_stats_row = require("./stats-row.js");
30
+ var import_provider_switcher = require("./provider-switcher.js");
31
+ var import_registry_view = require("./registry-view.js");
32
+ var import_buffers_view = require("./buffers-view.js");
33
+ var import_buffers_modal = require("./buffers-modal.js");
34
+ var import_devtools_client = require("./devtools-client.js");
35
+ let _unclaimedPane = null;
36
+ function claimPane(bundle) {
37
+ if (_unclaimedPane === bundle) _unclaimedPane = null;
38
+ }
39
+ function createPane(options = {}) {
40
+ const {
41
+ title = "Controls",
42
+ expanded = true,
43
+ driver = "raf",
44
+ ...rest
45
+ } = options;
46
+ if (_unclaimedPane) {
47
+ _unclaimedPane.pane.dispose();
48
+ _unclaimedPane = null;
49
+ }
50
+ const pane = new import_tweakpane.Pane({ title, expanded, ...rest });
51
+ pane.element.style.zIndex = "1000";
52
+ const wrapper = pane.element.closest(".tp-dfwv");
53
+ if (wrapper && wrapper !== pane.element) {
54
+ wrapper.style.zIndex = "1000";
55
+ }
56
+ (0, import_theme.applyTheme)(pane.element, import_theme.FLATLAND_THEME);
57
+ (0, import_plugins.registerPlugins)(pane);
58
+ pane.element.classList.add("tp-flatland-dimmable");
59
+ const header = pane.element.querySelector(".tp-rotv_b");
60
+ if (header) {
61
+ const pin = document.createElement("span");
62
+ pin.className = "tp-flatland-pin";
63
+ pin.setAttribute("role", "button");
64
+ pin.setAttribute("aria-label", "Keep pane opaque");
65
+ pin.setAttribute("aria-pressed", "false");
66
+ pin.tabIndex = 0;
67
+ pin.title = "Keep pane opaque";
68
+ pin.innerHTML = '<svg width="10" height="10" viewBox="0 0 14 14"><circle cx="7" cy="7" r="4"/></svg>';
69
+ const togglePin = (e) => {
70
+ e.stopPropagation();
71
+ e.preventDefault();
72
+ const pinned = pane.element.classList.toggle("tp-flatland-pinned");
73
+ pin.setAttribute("aria-pressed", String(pinned));
74
+ };
75
+ pin.addEventListener("click", togglePin);
76
+ pin.addEventListener("mousedown", (e) => {
77
+ e.stopPropagation();
78
+ });
79
+ pin.addEventListener("keydown", (e) => {
80
+ if (e.key === "Enter" || e.key === " ") togglePin(e);
81
+ });
82
+ header.insertBefore(pin, header.firstChild);
83
+ }
84
+ let client = null;
85
+ let graph = null;
86
+ let statsRow = null;
87
+ let providerSwitcher = null;
88
+ let registryView = null;
89
+ let buffersView = null;
90
+ let buffersModal = null;
91
+ const ACTIVE_FEATURES = ["stats", "env", "registry", "buffers"];
92
+ let mode = expanded ? "full" : "collapsed";
93
+ const applyMode = () => {
94
+ pane.element.classList.toggle("tp-flatland-minimal", mode === "minimal");
95
+ if (pane.expanded !== (mode !== "collapsed")) {
96
+ pane.expanded = mode !== "collapsed";
97
+ }
98
+ if (modeToggle !== null) {
99
+ modeToggle.setAttribute("data-mode", mode);
100
+ modeToggle.setAttribute(
101
+ "aria-label",
102
+ mode === "full" ? "Full pane (click for minimal)" : mode === "minimal" ? "Minimal pane (click to collapse)" : "Collapsed (click to expand)"
103
+ );
104
+ }
105
+ const c = client;
106
+ if (c !== null) {
107
+ if (mode === "full") c.setFeatures(ACTIVE_FEATURES);
108
+ else if (mode === "minimal") c.setFeatures(["stats", "env"]);
109
+ else c.setFeatures([]);
110
+ }
111
+ buffersView?.setPaneFolded(mode !== "full");
112
+ };
113
+ const cycleMode = () => {
114
+ mode = mode === "full" ? "minimal" : mode === "minimal" ? "collapsed" : "full";
115
+ applyMode();
116
+ };
117
+ let modeToggle = null;
118
+ if (header) {
119
+ modeToggle = document.createElement("span");
120
+ modeToggle.className = "tp-flatland-mode";
121
+ modeToggle.setAttribute("role", "button");
122
+ modeToggle.setAttribute("tabindex", "0");
123
+ modeToggle.title = "Cycle pane mode: full \u2192 minimal \u2192 collapsed";
124
+ modeToggle.innerHTML = '<span class="tp-flatland-mode-glyph"></span>';
125
+ const go = (e) => {
126
+ e.preventDefault();
127
+ e.stopPropagation();
128
+ cycleMode();
129
+ };
130
+ modeToggle.addEventListener("click", go);
131
+ modeToggle.addEventListener("mousedown", (e) => e.stopPropagation());
132
+ modeToggle.addEventListener("keydown", (e) => {
133
+ if (e.key === "Enter" || e.key === " ") go(e);
134
+ });
135
+ header.appendChild(modeToggle);
136
+ const onHeaderClick = (e) => {
137
+ const target = e.target;
138
+ if (target === null) return;
139
+ if (target.closest(".tp-flatland-pin, .tp-flatland-mode") !== null) return;
140
+ e.preventDefault();
141
+ e.stopPropagation();
142
+ cycleMode();
143
+ };
144
+ header.addEventListener("click", onHeaderClick, true);
145
+ }
146
+ try {
147
+ client = new import_devtools_client.DevtoolsClient({ features: ACTIVE_FEATURES });
148
+ graph = (0, import_stats_graph.addStatsGraph)(pane, client, { driver });
149
+ statsRow = (0, import_stats_row.addStatsRow)(pane, client);
150
+ providerSwitcher = (0, import_provider_switcher.addProviderSwitcher)(pane, client);
151
+ registryView = (0, import_registry_view.addRegistryView)(pane, client);
152
+ buffersModal = (0, import_buffers_modal.createBuffersModal)(client, {
153
+ onActiveChange: (name) => buffersView?.setActiveFromModal(name),
154
+ onOpen: () => buffersView?.setModalOpen(true),
155
+ onClose: () => buffersView?.setModalOpen(false)
156
+ });
157
+ buffersView = (0, import_buffers_view.addBuffersView)(pane, client, {
158
+ onExpand: (name) => buffersModal?.open(name)
159
+ });
160
+ client.start();
161
+ graph?.element.classList.add("tp-flatland-minimal-keep");
162
+ statsRow?.element.classList.add("tp-flatland-minimal-keep");
163
+ applyMode();
164
+ pane.on("fold", (ev) => {
165
+ const desired = ev.expanded ? mode === "collapsed" ? "full" : mode : "collapsed";
166
+ if (desired !== mode) {
167
+ mode = desired;
168
+ applyMode();
169
+ }
170
+ });
171
+ } catch {
172
+ }
173
+ let disposed = false;
174
+ const originalDispose = pane.dispose.bind(pane);
175
+ pane.dispose = () => {
176
+ if (disposed) return;
177
+ disposed = true;
178
+ graph?.dispose();
179
+ statsRow?.dispose();
180
+ providerSwitcher?.dispose();
181
+ registryView?.dispose();
182
+ buffersView?.dispose();
183
+ buffersModal?.dispose();
184
+ client?.dispose();
185
+ if (_unclaimedPane === bundle) _unclaimedPane = null;
186
+ originalDispose();
187
+ };
188
+ const bundle = {
189
+ pane,
190
+ update: () => graph?.update(),
191
+ get disposed() {
192
+ return disposed;
193
+ }
194
+ };
195
+ _unclaimedPane = bundle;
196
+ return bundle;
197
+ }
198
+ // Annotate the CommonJS export names for ESM import in node:
199
+ 0 && (module.exports = {
200
+ claimPane,
201
+ createPane
202
+ });
203
+ //# sourceMappingURL=create-pane.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/create-pane.ts"],"sourcesContent":["import { Pane } from 'tweakpane'\nimport { applyTheme, FLATLAND_THEME } from './theme.js'\nimport { registerPlugins } from './plugins.js'\nimport { addStatsGraph, type StatsGraphHandle } from './stats-graph.js'\nimport { addStatsRow, type StatsRowHandle } from './stats-row.js'\nimport { addProviderSwitcher, type ProviderSwitcherHandle } from './provider-switcher.js'\nimport { addRegistryView, type RegistryViewHandle } from './registry-view.js'\nimport { addBuffersView, type BuffersViewHandle } from './buffers-view.js'\nimport { createBuffersModal, type BuffersModalHandle } from './buffers-modal.js'\nimport { DevtoolsClient } from './devtools-client.js'\n\nexport interface CreatePaneOptions {\n /** Custom container element */\n container?: HTMLElement\n /** Pane title (default: 'Controls') */\n title?: string\n /** Default expansion state (default: true) */\n expanded?: boolean\n /**\n * Who drives the stats-graph render?\n * - `'raf'` (default): the pane starts its own `requestAnimationFrame`\n * loop. Zero setup; fine for simple apps.\n * - `'manual'`: no internal rAF. Call `bundle.update()` from your own\n * frame tick — `renderer.setAnimationLoop` callback, R3F `useFrame`,\n * or any ticker you already own. Preferred when you have a render\n * loop; avoids a second rAF (which Safari throttles).\n */\n driver?: 'raf' | 'manual'\n}\n\nexport interface PaneBundle {\n pane: Pane\n /**\n * Render a stats-graph frame. Call from your existing animation loop\n * (R3F `useFrame`, `renderer.setAnimationLoop`, etc.) for shared-tick\n * visuals — avoids a second `requestAnimationFrame` callback, which\n * Safari penalises.\n *\n * If you never call `update()`, the graph falls back to its own rAF\n * so the pane works out-of-the-box in apps without a render loop.\n */\n update(): void\n /**\n * `true` once `pane.dispose()` has been called. Lets React hooks\n * detect that a prior cleanup tore the bundle down and decide\n * whether to recreate (StrictMode mount→cleanup→mount).\n */\n readonly disposed: boolean\n}\n\n/**\n * Module-level \"unclaimed pane\" slot.\n *\n * React 18+ with StrictMode and Suspense can render a component multiple\n * times before the first effect commits (each aborted render discards its\n * `useRef` slots, including `bundleRef`). Because `new Pane()` mounts a\n * wrapper element to `document.body` synchronously, every aborted attempt\n * would leak a pane into the DOM (each with its own RAF loop).\n *\n * This slot tracks the most recent pane that hasn't been \"claimed\" by a\n * committed `useEffect`. When `createPane` is called again, any still-\n * unclaimed pane is disposed first — cleaning up orphans from the previous\n * discarded render. Once `usePane`'s effect commits, it calls `claimPane`\n * to prevent the current pane from being disposed by future `createPane`\n * calls in unrelated components.\n */\nlet _unclaimedPane: PaneBundle | null = null\n\nexport function claimPane(bundle: PaneBundle): void {\n if (_unclaimedPane === bundle) _unclaimedPane = null\n}\n\n/**\n * Create a themed Tweakpane instance with collapsible header and stats.\n *\n * Layout:\n * [Controls ▾] ← collapsible pane header\n * ┌─ FPS/MS/MEM graph ─┐ ← always visible (click to cycle)\n * ├─ Stats (folder) ───┤ ← collapsible: draws, tris\n * ├─ ...user controls...┤\n * └────────────────────┘\n *\n * @param options - Title, expansion state, and stats-graph `driver` (see\n * {@link CreatePaneOptions}). Defaults to a self-ticking `'raf'` pane\n * titled \"Controls\".\n * @returns A {@link PaneBundle} — the Tweakpane `pane` plus an `update()`\n * tick and a `disposed` flag.\n *\n * @example\n * ```typescript\n * const { pane } = createPane({ title: 'Debug' })\n * const folder = pane.addFolder({ title: 'Sprite' })\n * folder.addBinding(state, 'scale', { min: 0.1, max: 4 })\n * ```\n */\nexport function createPane(options: CreatePaneOptions = {}): PaneBundle {\n const {\n title = 'Controls',\n expanded = true,\n driver = 'raf',\n ...rest\n } = options\n\n // Dispose any orphaned pane from a previous (aborted) render attempt.\n if (_unclaimedPane) {\n _unclaimedPane.pane.dispose()\n _unclaimedPane = null\n }\n\n const pane = new Pane({ title, expanded, ...rest })\n\n // Ensure pane floats above canvas elements (R3F creates full-viewport divs).\n // Tweakpane wraps `pane.element` in a `.tp-dfwv` default-wrapper that's the\n // actual body sibling and stacks against other overlays — z-index on the\n // inner element would be a no-op. Apply to the wrapper when it exists.\n pane.element.style.zIndex = '1000'\n const wrapper = pane.element.closest<HTMLElement>('.tp-dfwv')\n if (wrapper && wrapper !== pane.element) {\n wrapper.style.zIndex = '1000'\n }\n\n applyTheme(pane.element, FLATLAND_THEME)\n registerPlugins(pane)\n\n // Idle-dimming + pin toggle. The pane is translucent while the mouse\n // isn't over it, fully opaque on hover. Clicking the pin in the header\n // locks it to fully opaque regardless of hover. See theme.ts for the\n // CSS that backs the `tp-flatland-dimmable` / `tp-flatland-pinned`\n // classes and the `tp-flatland-pin` element.\n pane.element.classList.add('tp-flatland-dimmable')\n const header = pane.element.querySelector<HTMLElement>('.tp-rotv_b')\n if (header) {\n const pin = document.createElement('span')\n pin.className = 'tp-flatland-pin'\n pin.setAttribute('role', 'button')\n pin.setAttribute('aria-label', 'Keep pane opaque')\n pin.setAttribute('aria-pressed', 'false')\n pin.tabIndex = 0\n pin.title = 'Keep pane opaque'\n pin.innerHTML = '<svg width=\"10\" height=\"10\" viewBox=\"0 0 14 14\"><circle cx=\"7\" cy=\"7\" r=\"4\"/></svg>'\n\n const togglePin = (e: Event) => {\n e.stopPropagation()\n e.preventDefault()\n const pinned = pane.element.classList.toggle('tp-flatland-pinned')\n pin.setAttribute('aria-pressed', String(pinned))\n }\n pin.addEventListener('click', togglePin)\n // Stop mousedown so the header button doesn't flash its :active state\n // (and doesn't gain focus) when clicking the pin.\n pin.addEventListener('mousedown', (e) => {\n e.stopPropagation()\n })\n pin.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') togglePin(e)\n })\n\n header.insertBefore(pin, header.firstChild)\n }\n\n // Always-on stats graph + row, bus-driven. A single `DevtoolsClient`\n // feeds both. If the bus isn't available in the environment (e.g., a\n // test runner without BroadcastChannel), the try/catch swallows it\n // and the pane still works for user controls — just no stats display.\n let client: DevtoolsClient | null = null\n let graph: StatsGraphHandle | null = null\n let statsRow: StatsRowHandle | null = null\n let providerSwitcher: ProviderSwitcherHandle | null = null\n let registryView: RegistryViewHandle | null = null\n let buffersView: BuffersViewHandle | null = null\n let buffersModal: BuffersModalHandle | null = null\n\n const ACTIVE_FEATURES: ('stats' | 'env' | 'registry' | 'buffers')[] = ['stats', 'env', 'registry', 'buffers']\n\n // Three-mode cycle — full → minimal → collapsed → full. Replaces\n // Tweakpane's binary fold with something more useful: \"minimal\" keeps\n // only the stats graph + draws/tris row visible (no buffers, no\n // registry, no provider switcher, no user-added controls) so you can\n // watch perf numbers without a giant pane covering the viewport, AND\n // without paying the cost of buffer streaming.\n type Mode = 'full' | 'minimal' | 'collapsed'\n let mode: Mode = expanded ? 'full' : 'collapsed'\n\n const applyMode = (): void => {\n pane.element.classList.toggle('tp-flatland-minimal', mode === 'minimal')\n // 'full' and 'minimal' both have the pane expanded at the TP level;\n // 'collapsed' folds it.\n if (pane.expanded !== (mode !== 'collapsed')) {\n pane.expanded = mode !== 'collapsed'\n }\n // Our own caret icon reflects the cycle position.\n if (modeToggle !== null) {\n modeToggle.setAttribute('data-mode', mode)\n modeToggle.setAttribute(\n 'aria-label',\n mode === 'full' ? 'Full pane (click for minimal)'\n : mode === 'minimal' ? 'Minimal pane (click to collapse)'\n : 'Collapsed (click to expand)',\n )\n }\n const c = client\n if (c !== null) {\n // Feature gating follows the mode:\n // full → everything\n // minimal → stats + env only; no registry, no buffers drain\n // collapsed → nothing\n if (mode === 'full') c.setFeatures(ACTIVE_FEATURES)\n else if (mode === 'minimal') c.setFeatures(['stats', 'env'])\n else c.setFeatures([])\n }\n // Propagate into the buffers view so it knows to unsubscribe. Both\n // 'minimal' and 'collapsed' ask the view to treat itself as folded\n // (no thumbnail stream, no GPU readback on the provider).\n buffersView?.setPaneFolded(mode !== 'full')\n }\n\n const cycleMode = (): void => {\n mode = mode === 'full' ? 'minimal' : mode === 'minimal' ? 'collapsed' : 'full'\n applyMode()\n }\n\n // Replace Tweakpane's built-in fold caret with our own mode-cycle\n // button AND intercept clicks anywhere on the pane header so the\n // native binary-fold behavior doesn't skip 'minimal'. The native\n // caret is hidden via CSS (.tp-rotv_m display:none).\n let modeToggle: HTMLElement | null = null\n if (header) {\n modeToggle = document.createElement('span')\n modeToggle.className = 'tp-flatland-mode'\n modeToggle.setAttribute('role', 'button')\n modeToggle.setAttribute('tabindex', '0')\n modeToggle.title = 'Cycle pane mode: full → minimal → collapsed'\n // Single caret glyph — CSS rotates it based on data-mode so one\n // visual vocabulary handles all three states. Matches tweakpane's\n // native rotating-caret convention and gives a screw-head look at\n // 45° for minimal.\n modeToggle.innerHTML = '<span class=\"tp-flatland-mode-glyph\"></span>'\n const go = (e: Event): void => {\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n modeToggle.addEventListener('click', go)\n modeToggle.addEventListener('mousedown', (e) => e.stopPropagation())\n modeToggle.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') go(e)\n })\n header.appendChild(modeToggle)\n\n // Capture-phase click on the header itself — Tweakpane's root\n // component wires a listener that toggles `expanded`, which would\n // skip our tri-state. Intercept before it fires, cycle our modes,\n // stop propagation so the default toggle never runs. Clicks on our\n // own overlay buttons (pin, mode toggle) already stop propagation\n // themselves, so they're unaffected.\n const onHeaderClick = (e: MouseEvent): void => {\n const target = e.target as HTMLElement | null\n if (target === null) return\n // Let clicks on our own interactive children through untouched.\n if (target.closest('.tp-flatland-pin, .tp-flatland-mode') !== null) return\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n header.addEventListener('click', onHeaderClick, true)\n }\n\n try {\n client = new DevtoolsClient({ features: ACTIVE_FEATURES })\n graph = addStatsGraph(pane, client, { driver })\n statsRow = addStatsRow(pane, client)\n // Hidden until two or more providers announce.\n providerSwitcher = addProviderSwitcher(pane, client)\n // Hidden until the provider publishes at least one entry via\n // `registerDebugArray`.\n registryView = addRegistryView(pane, client)\n // Modal is built lazily — `createBuffersModal` mounts the DOM\n // immediately (display:none) so the first expand is instant.\n buffersModal = createBuffersModal(client, {\n onActiveChange: (name) => buffersView?.setActiveFromModal(name),\n onOpen: () => buffersView?.setModalOpen(true),\n onClose: () => buffersView?.setModalOpen(false),\n })\n // Hidden until the provider publishes at least one debug texture\n // via `registerDebugTexture`. ⤢ in-blade button calls into the\n // modal handle.\n buffersView = addBuffersView(pane, client, {\n onExpand: (name) => buffersModal?.open(name),\n })\n client.start()\n\n // Mark the always-visible blades so CSS can keep them shown while\n // hiding everything else in 'minimal' mode.\n graph?.element.classList.add('tp-flatland-minimal-keep')\n statsRow?.element.classList.add('tp-flatland-minimal-keep')\n\n applyMode()\n\n // Tweakpane still tries to toggle expanded when the user clicks the\n // header — intercept and redirect through our cycle. The `fold`\n // event fires from keyboard ⇥-space or external sets; keep it as a\n // fallback that syncs our mode if something else flips expansion.\n pane.on('fold', (ev) => {\n const desired: Mode = ev.expanded\n ? (mode === 'collapsed' ? 'full' : mode)\n : 'collapsed'\n if (desired !== mode) {\n mode = desired\n applyMode()\n }\n })\n } catch {\n // Bus unavailable — skip the stats blades. Pane still usable.\n }\n\n // Clean up on dispose. Idempotent: calling `dispose()` twice (e.g.,\n // once by the user and once by a later `createPane` disposing the\n // unclaimed slot) no-ops the second time instead of throwing.\n let disposed = false\n const originalDispose = pane.dispose.bind(pane)\n pane.dispose = () => {\n if (disposed) return\n disposed = true\n graph?.dispose()\n statsRow?.dispose()\n providerSwitcher?.dispose()\n registryView?.dispose()\n buffersView?.dispose()\n buffersModal?.dispose()\n client?.dispose()\n // If the user disposed the pane directly without claiming, free\n // the slot so the next createPane doesn't try to dispose us again.\n if (_unclaimedPane === bundle) _unclaimedPane = null\n originalDispose()\n }\n\n const bundle: PaneBundle = {\n pane,\n update: () => graph?.update(),\n get disposed() {\n return disposed\n },\n }\n _unclaimedPane = bundle\n return bundle\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,66 @@
1
+ import { Pane } from 'tweakpane';
2
+
3
+ interface CreatePaneOptions {
4
+ /** Custom container element */
5
+ container?: HTMLElement;
6
+ /** Pane title (default: 'Controls') */
7
+ title?: string;
8
+ /** Default expansion state (default: true) */
9
+ expanded?: boolean;
10
+ /**
11
+ * Who drives the stats-graph render?
12
+ * - `'raf'` (default): the pane starts its own `requestAnimationFrame`
13
+ * loop. Zero setup; fine for simple apps.
14
+ * - `'manual'`: no internal rAF. Call `bundle.update()` from your own
15
+ * frame tick — `renderer.setAnimationLoop` callback, R3F `useFrame`,
16
+ * or any ticker you already own. Preferred when you have a render
17
+ * loop; avoids a second rAF (which Safari throttles).
18
+ */
19
+ driver?: 'raf' | 'manual';
20
+ }
21
+ interface PaneBundle {
22
+ pane: Pane;
23
+ /**
24
+ * Render a stats-graph frame. Call from your existing animation loop
25
+ * (R3F `useFrame`, `renderer.setAnimationLoop`, etc.) for shared-tick
26
+ * visuals — avoids a second `requestAnimationFrame` callback, which
27
+ * Safari penalises.
28
+ *
29
+ * If you never call `update()`, the graph falls back to its own rAF
30
+ * so the pane works out-of-the-box in apps without a render loop.
31
+ */
32
+ update(): void;
33
+ /**
34
+ * `true` once `pane.dispose()` has been called. Lets React hooks
35
+ * detect that a prior cleanup tore the bundle down and decide
36
+ * whether to recreate (StrictMode mount→cleanup→mount).
37
+ */
38
+ readonly disposed: boolean;
39
+ }
40
+ declare function claimPane(bundle: PaneBundle): void;
41
+ /**
42
+ * Create a themed Tweakpane instance with collapsible header and stats.
43
+ *
44
+ * Layout:
45
+ * [Controls ▾] ← collapsible pane header
46
+ * ┌─ FPS/MS/MEM graph ─┐ ← always visible (click to cycle)
47
+ * ├─ Stats (folder) ───┤ ← collapsible: draws, tris
48
+ * ├─ ...user controls...┤
49
+ * └────────────────────┘
50
+ *
51
+ * @param options - Title, expansion state, and stats-graph `driver` (see
52
+ * {@link CreatePaneOptions}). Defaults to a self-ticking `'raf'` pane
53
+ * titled "Controls".
54
+ * @returns A {@link PaneBundle} — the Tweakpane `pane` plus an `update()`
55
+ * tick and a `disposed` flag.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const { pane } = createPane({ title: 'Debug' })
60
+ * const folder = pane.addFolder({ title: 'Sprite' })
61
+ * folder.addBinding(state, 'scale', { min: 0.1, max: 4 })
62
+ * ```
63
+ */
64
+ declare function createPane(options?: CreatePaneOptions): PaneBundle;
65
+
66
+ export { type CreatePaneOptions, type PaneBundle, claimPane, createPane };
@@ -0,0 +1,66 @@
1
+ import { Pane } from 'tweakpane';
2
+
3
+ interface CreatePaneOptions {
4
+ /** Custom container element */
5
+ container?: HTMLElement;
6
+ /** Pane title (default: 'Controls') */
7
+ title?: string;
8
+ /** Default expansion state (default: true) */
9
+ expanded?: boolean;
10
+ /**
11
+ * Who drives the stats-graph render?
12
+ * - `'raf'` (default): the pane starts its own `requestAnimationFrame`
13
+ * loop. Zero setup; fine for simple apps.
14
+ * - `'manual'`: no internal rAF. Call `bundle.update()` from your own
15
+ * frame tick — `renderer.setAnimationLoop` callback, R3F `useFrame`,
16
+ * or any ticker you already own. Preferred when you have a render
17
+ * loop; avoids a second rAF (which Safari throttles).
18
+ */
19
+ driver?: 'raf' | 'manual';
20
+ }
21
+ interface PaneBundle {
22
+ pane: Pane;
23
+ /**
24
+ * Render a stats-graph frame. Call from your existing animation loop
25
+ * (R3F `useFrame`, `renderer.setAnimationLoop`, etc.) for shared-tick
26
+ * visuals — avoids a second `requestAnimationFrame` callback, which
27
+ * Safari penalises.
28
+ *
29
+ * If you never call `update()`, the graph falls back to its own rAF
30
+ * so the pane works out-of-the-box in apps without a render loop.
31
+ */
32
+ update(): void;
33
+ /**
34
+ * `true` once `pane.dispose()` has been called. Lets React hooks
35
+ * detect that a prior cleanup tore the bundle down and decide
36
+ * whether to recreate (StrictMode mount→cleanup→mount).
37
+ */
38
+ readonly disposed: boolean;
39
+ }
40
+ declare function claimPane(bundle: PaneBundle): void;
41
+ /**
42
+ * Create a themed Tweakpane instance with collapsible header and stats.
43
+ *
44
+ * Layout:
45
+ * [Controls ▾] ← collapsible pane header
46
+ * ┌─ FPS/MS/MEM graph ─┐ ← always visible (click to cycle)
47
+ * ├─ Stats (folder) ───┤ ← collapsible: draws, tris
48
+ * ├─ ...user controls...┤
49
+ * └────────────────────┘
50
+ *
51
+ * @param options - Title, expansion state, and stats-graph `driver` (see
52
+ * {@link CreatePaneOptions}). Defaults to a self-ticking `'raf'` pane
53
+ * titled "Controls".
54
+ * @returns A {@link PaneBundle} — the Tweakpane `pane` plus an `update()`
55
+ * tick and a `disposed` flag.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const { pane } = createPane({ title: 'Debug' })
60
+ * const folder = pane.addFolder({ title: 'Sprite' })
61
+ * folder.addBinding(state, 'scale', { min: 0.1, max: 4 })
62
+ * ```
63
+ */
64
+ declare function createPane(options?: CreatePaneOptions): PaneBundle;
65
+
66
+ export { type CreatePaneOptions, type PaneBundle, claimPane, createPane };
@@ -0,0 +1,178 @@
1
+ import { Pane } from "tweakpane";
2
+ import { applyTheme, FLATLAND_THEME } from "./theme.js";
3
+ import { registerPlugins } from "./plugins.js";
4
+ import { addStatsGraph } from "./stats-graph.js";
5
+ import { addStatsRow } from "./stats-row.js";
6
+ import { addProviderSwitcher } from "./provider-switcher.js";
7
+ import { addRegistryView } from "./registry-view.js";
8
+ import { addBuffersView } from "./buffers-view.js";
9
+ import { createBuffersModal } from "./buffers-modal.js";
10
+ import { DevtoolsClient } from "./devtools-client.js";
11
+ let _unclaimedPane = null;
12
+ function claimPane(bundle) {
13
+ if (_unclaimedPane === bundle) _unclaimedPane = null;
14
+ }
15
+ function createPane(options = {}) {
16
+ const {
17
+ title = "Controls",
18
+ expanded = true,
19
+ driver = "raf",
20
+ ...rest
21
+ } = options;
22
+ if (_unclaimedPane) {
23
+ _unclaimedPane.pane.dispose();
24
+ _unclaimedPane = null;
25
+ }
26
+ const pane = new Pane({ title, expanded, ...rest });
27
+ pane.element.style.zIndex = "1000";
28
+ const wrapper = pane.element.closest(".tp-dfwv");
29
+ if (wrapper && wrapper !== pane.element) {
30
+ wrapper.style.zIndex = "1000";
31
+ }
32
+ applyTheme(pane.element, FLATLAND_THEME);
33
+ registerPlugins(pane);
34
+ pane.element.classList.add("tp-flatland-dimmable");
35
+ const header = pane.element.querySelector(".tp-rotv_b");
36
+ if (header) {
37
+ const pin = document.createElement("span");
38
+ pin.className = "tp-flatland-pin";
39
+ pin.setAttribute("role", "button");
40
+ pin.setAttribute("aria-label", "Keep pane opaque");
41
+ pin.setAttribute("aria-pressed", "false");
42
+ pin.tabIndex = 0;
43
+ pin.title = "Keep pane opaque";
44
+ pin.innerHTML = '<svg width="10" height="10" viewBox="0 0 14 14"><circle cx="7" cy="7" r="4"/></svg>';
45
+ const togglePin = (e) => {
46
+ e.stopPropagation();
47
+ e.preventDefault();
48
+ const pinned = pane.element.classList.toggle("tp-flatland-pinned");
49
+ pin.setAttribute("aria-pressed", String(pinned));
50
+ };
51
+ pin.addEventListener("click", togglePin);
52
+ pin.addEventListener("mousedown", (e) => {
53
+ e.stopPropagation();
54
+ });
55
+ pin.addEventListener("keydown", (e) => {
56
+ if (e.key === "Enter" || e.key === " ") togglePin(e);
57
+ });
58
+ header.insertBefore(pin, header.firstChild);
59
+ }
60
+ let client = null;
61
+ let graph = null;
62
+ let statsRow = null;
63
+ let providerSwitcher = null;
64
+ let registryView = null;
65
+ let buffersView = null;
66
+ let buffersModal = null;
67
+ const ACTIVE_FEATURES = ["stats", "env", "registry", "buffers"];
68
+ let mode = expanded ? "full" : "collapsed";
69
+ const applyMode = () => {
70
+ pane.element.classList.toggle("tp-flatland-minimal", mode === "minimal");
71
+ if (pane.expanded !== (mode !== "collapsed")) {
72
+ pane.expanded = mode !== "collapsed";
73
+ }
74
+ if (modeToggle !== null) {
75
+ modeToggle.setAttribute("data-mode", mode);
76
+ modeToggle.setAttribute(
77
+ "aria-label",
78
+ mode === "full" ? "Full pane (click for minimal)" : mode === "minimal" ? "Minimal pane (click to collapse)" : "Collapsed (click to expand)"
79
+ );
80
+ }
81
+ const c = client;
82
+ if (c !== null) {
83
+ if (mode === "full") c.setFeatures(ACTIVE_FEATURES);
84
+ else if (mode === "minimal") c.setFeatures(["stats", "env"]);
85
+ else c.setFeatures([]);
86
+ }
87
+ buffersView?.setPaneFolded(mode !== "full");
88
+ };
89
+ const cycleMode = () => {
90
+ mode = mode === "full" ? "minimal" : mode === "minimal" ? "collapsed" : "full";
91
+ applyMode();
92
+ };
93
+ let modeToggle = null;
94
+ if (header) {
95
+ modeToggle = document.createElement("span");
96
+ modeToggle.className = "tp-flatland-mode";
97
+ modeToggle.setAttribute("role", "button");
98
+ modeToggle.setAttribute("tabindex", "0");
99
+ modeToggle.title = "Cycle pane mode: full \u2192 minimal \u2192 collapsed";
100
+ modeToggle.innerHTML = '<span class="tp-flatland-mode-glyph"></span>';
101
+ const go = (e) => {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ cycleMode();
105
+ };
106
+ modeToggle.addEventListener("click", go);
107
+ modeToggle.addEventListener("mousedown", (e) => e.stopPropagation());
108
+ modeToggle.addEventListener("keydown", (e) => {
109
+ if (e.key === "Enter" || e.key === " ") go(e);
110
+ });
111
+ header.appendChild(modeToggle);
112
+ const onHeaderClick = (e) => {
113
+ const target = e.target;
114
+ if (target === null) return;
115
+ if (target.closest(".tp-flatland-pin, .tp-flatland-mode") !== null) return;
116
+ e.preventDefault();
117
+ e.stopPropagation();
118
+ cycleMode();
119
+ };
120
+ header.addEventListener("click", onHeaderClick, true);
121
+ }
122
+ try {
123
+ client = new DevtoolsClient({ features: ACTIVE_FEATURES });
124
+ graph = addStatsGraph(pane, client, { driver });
125
+ statsRow = addStatsRow(pane, client);
126
+ providerSwitcher = addProviderSwitcher(pane, client);
127
+ registryView = addRegistryView(pane, client);
128
+ buffersModal = createBuffersModal(client, {
129
+ onActiveChange: (name) => buffersView?.setActiveFromModal(name),
130
+ onOpen: () => buffersView?.setModalOpen(true),
131
+ onClose: () => buffersView?.setModalOpen(false)
132
+ });
133
+ buffersView = addBuffersView(pane, client, {
134
+ onExpand: (name) => buffersModal?.open(name)
135
+ });
136
+ client.start();
137
+ graph?.element.classList.add("tp-flatland-minimal-keep");
138
+ statsRow?.element.classList.add("tp-flatland-minimal-keep");
139
+ applyMode();
140
+ pane.on("fold", (ev) => {
141
+ const desired = ev.expanded ? mode === "collapsed" ? "full" : mode : "collapsed";
142
+ if (desired !== mode) {
143
+ mode = desired;
144
+ applyMode();
145
+ }
146
+ });
147
+ } catch {
148
+ }
149
+ let disposed = false;
150
+ const originalDispose = pane.dispose.bind(pane);
151
+ pane.dispose = () => {
152
+ if (disposed) return;
153
+ disposed = true;
154
+ graph?.dispose();
155
+ statsRow?.dispose();
156
+ providerSwitcher?.dispose();
157
+ registryView?.dispose();
158
+ buffersView?.dispose();
159
+ buffersModal?.dispose();
160
+ client?.dispose();
161
+ if (_unclaimedPane === bundle) _unclaimedPane = null;
162
+ originalDispose();
163
+ };
164
+ const bundle = {
165
+ pane,
166
+ update: () => graph?.update(),
167
+ get disposed() {
168
+ return disposed;
169
+ }
170
+ };
171
+ _unclaimedPane = bundle;
172
+ return bundle;
173
+ }
174
+ export {
175
+ claimPane,
176
+ createPane
177
+ };
178
+ //# sourceMappingURL=create-pane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/create-pane.ts"],"sourcesContent":["import { Pane } from 'tweakpane'\nimport { applyTheme, FLATLAND_THEME } from './theme.js'\nimport { registerPlugins } from './plugins.js'\nimport { addStatsGraph, type StatsGraphHandle } from './stats-graph.js'\nimport { addStatsRow, type StatsRowHandle } from './stats-row.js'\nimport { addProviderSwitcher, type ProviderSwitcherHandle } from './provider-switcher.js'\nimport { addRegistryView, type RegistryViewHandle } from './registry-view.js'\nimport { addBuffersView, type BuffersViewHandle } from './buffers-view.js'\nimport { createBuffersModal, type BuffersModalHandle } from './buffers-modal.js'\nimport { DevtoolsClient } from './devtools-client.js'\n\nexport interface CreatePaneOptions {\n /** Custom container element */\n container?: HTMLElement\n /** Pane title (default: 'Controls') */\n title?: string\n /** Default expansion state (default: true) */\n expanded?: boolean\n /**\n * Who drives the stats-graph render?\n * - `'raf'` (default): the pane starts its own `requestAnimationFrame`\n * loop. Zero setup; fine for simple apps.\n * - `'manual'`: no internal rAF. Call `bundle.update()` from your own\n * frame tick — `renderer.setAnimationLoop` callback, R3F `useFrame`,\n * or any ticker you already own. Preferred when you have a render\n * loop; avoids a second rAF (which Safari throttles).\n */\n driver?: 'raf' | 'manual'\n}\n\nexport interface PaneBundle {\n pane: Pane\n /**\n * Render a stats-graph frame. Call from your existing animation loop\n * (R3F `useFrame`, `renderer.setAnimationLoop`, etc.) for shared-tick\n * visuals — avoids a second `requestAnimationFrame` callback, which\n * Safari penalises.\n *\n * If you never call `update()`, the graph falls back to its own rAF\n * so the pane works out-of-the-box in apps without a render loop.\n */\n update(): void\n /**\n * `true` once `pane.dispose()` has been called. Lets React hooks\n * detect that a prior cleanup tore the bundle down and decide\n * whether to recreate (StrictMode mount→cleanup→mount).\n */\n readonly disposed: boolean\n}\n\n/**\n * Module-level \"unclaimed pane\" slot.\n *\n * React 18+ with StrictMode and Suspense can render a component multiple\n * times before the first effect commits (each aborted render discards its\n * `useRef` slots, including `bundleRef`). Because `new Pane()` mounts a\n * wrapper element to `document.body` synchronously, every aborted attempt\n * would leak a pane into the DOM (each with its own RAF loop).\n *\n * This slot tracks the most recent pane that hasn't been \"claimed\" by a\n * committed `useEffect`. When `createPane` is called again, any still-\n * unclaimed pane is disposed first — cleaning up orphans from the previous\n * discarded render. Once `usePane`'s effect commits, it calls `claimPane`\n * to prevent the current pane from being disposed by future `createPane`\n * calls in unrelated components.\n */\nlet _unclaimedPane: PaneBundle | null = null\n\nexport function claimPane(bundle: PaneBundle): void {\n if (_unclaimedPane === bundle) _unclaimedPane = null\n}\n\n/**\n * Create a themed Tweakpane instance with collapsible header and stats.\n *\n * Layout:\n * [Controls ▾] ← collapsible pane header\n * ┌─ FPS/MS/MEM graph ─┐ ← always visible (click to cycle)\n * ├─ Stats (folder) ───┤ ← collapsible: draws, tris\n * ├─ ...user controls...┤\n * └────────────────────┘\n *\n * @param options - Title, expansion state, and stats-graph `driver` (see\n * {@link CreatePaneOptions}). Defaults to a self-ticking `'raf'` pane\n * titled \"Controls\".\n * @returns A {@link PaneBundle} — the Tweakpane `pane` plus an `update()`\n * tick and a `disposed` flag.\n *\n * @example\n * ```typescript\n * const { pane } = createPane({ title: 'Debug' })\n * const folder = pane.addFolder({ title: 'Sprite' })\n * folder.addBinding(state, 'scale', { min: 0.1, max: 4 })\n * ```\n */\nexport function createPane(options: CreatePaneOptions = {}): PaneBundle {\n const {\n title = 'Controls',\n expanded = true,\n driver = 'raf',\n ...rest\n } = options\n\n // Dispose any orphaned pane from a previous (aborted) render attempt.\n if (_unclaimedPane) {\n _unclaimedPane.pane.dispose()\n _unclaimedPane = null\n }\n\n const pane = new Pane({ title, expanded, ...rest })\n\n // Ensure pane floats above canvas elements (R3F creates full-viewport divs).\n // Tweakpane wraps `pane.element` in a `.tp-dfwv` default-wrapper that's the\n // actual body sibling and stacks against other overlays — z-index on the\n // inner element would be a no-op. Apply to the wrapper when it exists.\n pane.element.style.zIndex = '1000'\n const wrapper = pane.element.closest<HTMLElement>('.tp-dfwv')\n if (wrapper && wrapper !== pane.element) {\n wrapper.style.zIndex = '1000'\n }\n\n applyTheme(pane.element, FLATLAND_THEME)\n registerPlugins(pane)\n\n // Idle-dimming + pin toggle. The pane is translucent while the mouse\n // isn't over it, fully opaque on hover. Clicking the pin in the header\n // locks it to fully opaque regardless of hover. See theme.ts for the\n // CSS that backs the `tp-flatland-dimmable` / `tp-flatland-pinned`\n // classes and the `tp-flatland-pin` element.\n pane.element.classList.add('tp-flatland-dimmable')\n const header = pane.element.querySelector<HTMLElement>('.tp-rotv_b')\n if (header) {\n const pin = document.createElement('span')\n pin.className = 'tp-flatland-pin'\n pin.setAttribute('role', 'button')\n pin.setAttribute('aria-label', 'Keep pane opaque')\n pin.setAttribute('aria-pressed', 'false')\n pin.tabIndex = 0\n pin.title = 'Keep pane opaque'\n pin.innerHTML = '<svg width=\"10\" height=\"10\" viewBox=\"0 0 14 14\"><circle cx=\"7\" cy=\"7\" r=\"4\"/></svg>'\n\n const togglePin = (e: Event) => {\n e.stopPropagation()\n e.preventDefault()\n const pinned = pane.element.classList.toggle('tp-flatland-pinned')\n pin.setAttribute('aria-pressed', String(pinned))\n }\n pin.addEventListener('click', togglePin)\n // Stop mousedown so the header button doesn't flash its :active state\n // (and doesn't gain focus) when clicking the pin.\n pin.addEventListener('mousedown', (e) => {\n e.stopPropagation()\n })\n pin.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') togglePin(e)\n })\n\n header.insertBefore(pin, header.firstChild)\n }\n\n // Always-on stats graph + row, bus-driven. A single `DevtoolsClient`\n // feeds both. If the bus isn't available in the environment (e.g., a\n // test runner without BroadcastChannel), the try/catch swallows it\n // and the pane still works for user controls — just no stats display.\n let client: DevtoolsClient | null = null\n let graph: StatsGraphHandle | null = null\n let statsRow: StatsRowHandle | null = null\n let providerSwitcher: ProviderSwitcherHandle | null = null\n let registryView: RegistryViewHandle | null = null\n let buffersView: BuffersViewHandle | null = null\n let buffersModal: BuffersModalHandle | null = null\n\n const ACTIVE_FEATURES: ('stats' | 'env' | 'registry' | 'buffers')[] = ['stats', 'env', 'registry', 'buffers']\n\n // Three-mode cycle — full → minimal → collapsed → full. Replaces\n // Tweakpane's binary fold with something more useful: \"minimal\" keeps\n // only the stats graph + draws/tris row visible (no buffers, no\n // registry, no provider switcher, no user-added controls) so you can\n // watch perf numbers without a giant pane covering the viewport, AND\n // without paying the cost of buffer streaming.\n type Mode = 'full' | 'minimal' | 'collapsed'\n let mode: Mode = expanded ? 'full' : 'collapsed'\n\n const applyMode = (): void => {\n pane.element.classList.toggle('tp-flatland-minimal', mode === 'minimal')\n // 'full' and 'minimal' both have the pane expanded at the TP level;\n // 'collapsed' folds it.\n if (pane.expanded !== (mode !== 'collapsed')) {\n pane.expanded = mode !== 'collapsed'\n }\n // Our own caret icon reflects the cycle position.\n if (modeToggle !== null) {\n modeToggle.setAttribute('data-mode', mode)\n modeToggle.setAttribute(\n 'aria-label',\n mode === 'full' ? 'Full pane (click for minimal)'\n : mode === 'minimal' ? 'Minimal pane (click to collapse)'\n : 'Collapsed (click to expand)',\n )\n }\n const c = client\n if (c !== null) {\n // Feature gating follows the mode:\n // full → everything\n // minimal → stats + env only; no registry, no buffers drain\n // collapsed → nothing\n if (mode === 'full') c.setFeatures(ACTIVE_FEATURES)\n else if (mode === 'minimal') c.setFeatures(['stats', 'env'])\n else c.setFeatures([])\n }\n // Propagate into the buffers view so it knows to unsubscribe. Both\n // 'minimal' and 'collapsed' ask the view to treat itself as folded\n // (no thumbnail stream, no GPU readback on the provider).\n buffersView?.setPaneFolded(mode !== 'full')\n }\n\n const cycleMode = (): void => {\n mode = mode === 'full' ? 'minimal' : mode === 'minimal' ? 'collapsed' : 'full'\n applyMode()\n }\n\n // Replace Tweakpane's built-in fold caret with our own mode-cycle\n // button AND intercept clicks anywhere on the pane header so the\n // native binary-fold behavior doesn't skip 'minimal'. The native\n // caret is hidden via CSS (.tp-rotv_m display:none).\n let modeToggle: HTMLElement | null = null\n if (header) {\n modeToggle = document.createElement('span')\n modeToggle.className = 'tp-flatland-mode'\n modeToggle.setAttribute('role', 'button')\n modeToggle.setAttribute('tabindex', '0')\n modeToggle.title = 'Cycle pane mode: full → minimal → collapsed'\n // Single caret glyph — CSS rotates it based on data-mode so one\n // visual vocabulary handles all three states. Matches tweakpane's\n // native rotating-caret convention and gives a screw-head look at\n // 45° for minimal.\n modeToggle.innerHTML = '<span class=\"tp-flatland-mode-glyph\"></span>'\n const go = (e: Event): void => {\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n modeToggle.addEventListener('click', go)\n modeToggle.addEventListener('mousedown', (e) => e.stopPropagation())\n modeToggle.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') go(e)\n })\n header.appendChild(modeToggle)\n\n // Capture-phase click on the header itself — Tweakpane's root\n // component wires a listener that toggles `expanded`, which would\n // skip our tri-state. Intercept before it fires, cycle our modes,\n // stop propagation so the default toggle never runs. Clicks on our\n // own overlay buttons (pin, mode toggle) already stop propagation\n // themselves, so they're unaffected.\n const onHeaderClick = (e: MouseEvent): void => {\n const target = e.target as HTMLElement | null\n if (target === null) return\n // Let clicks on our own interactive children through untouched.\n if (target.closest('.tp-flatland-pin, .tp-flatland-mode') !== null) return\n e.preventDefault()\n e.stopPropagation()\n cycleMode()\n }\n header.addEventListener('click', onHeaderClick, true)\n }\n\n try {\n client = new DevtoolsClient({ features: ACTIVE_FEATURES })\n graph = addStatsGraph(pane, client, { driver })\n statsRow = addStatsRow(pane, client)\n // Hidden until two or more providers announce.\n providerSwitcher = addProviderSwitcher(pane, client)\n // Hidden until the provider publishes at least one entry via\n // `registerDebugArray`.\n registryView = addRegistryView(pane, client)\n // Modal is built lazily — `createBuffersModal` mounts the DOM\n // immediately (display:none) so the first expand is instant.\n buffersModal = createBuffersModal(client, {\n onActiveChange: (name) => buffersView?.setActiveFromModal(name),\n onOpen: () => buffersView?.setModalOpen(true),\n onClose: () => buffersView?.setModalOpen(false),\n })\n // Hidden until the provider publishes at least one debug texture\n // via `registerDebugTexture`. ⤢ in-blade button calls into the\n // modal handle.\n buffersView = addBuffersView(pane, client, {\n onExpand: (name) => buffersModal?.open(name),\n })\n client.start()\n\n // Mark the always-visible blades so CSS can keep them shown while\n // hiding everything else in 'minimal' mode.\n graph?.element.classList.add('tp-flatland-minimal-keep')\n statsRow?.element.classList.add('tp-flatland-minimal-keep')\n\n applyMode()\n\n // Tweakpane still tries to toggle expanded when the user clicks the\n // header — intercept and redirect through our cycle. The `fold`\n // event fires from keyboard ⇥-space or external sets; keep it as a\n // fallback that syncs our mode if something else flips expansion.\n pane.on('fold', (ev) => {\n const desired: Mode = ev.expanded\n ? (mode === 'collapsed' ? 'full' : mode)\n : 'collapsed'\n if (desired !== mode) {\n mode = desired\n applyMode()\n }\n })\n } catch {\n // Bus unavailable — skip the stats blades. Pane still usable.\n }\n\n // Clean up on dispose. Idempotent: calling `dispose()` twice (e.g.,\n // once by the user and once by a later `createPane` disposing the\n // unclaimed slot) no-ops the second time instead of throwing.\n let disposed = false\n const originalDispose = pane.dispose.bind(pane)\n pane.dispose = () => {\n if (disposed) return\n disposed = true\n graph?.dispose()\n statsRow?.dispose()\n providerSwitcher?.dispose()\n registryView?.dispose()\n buffersView?.dispose()\n buffersModal?.dispose()\n client?.dispose()\n // If the user disposed the pane directly without claiming, free\n // the slot so the next createPane doesn't try to dispose us again.\n if (_unclaimedPane === bundle) _unclaimedPane = null\n originalDispose()\n }\n\n const bundle: PaneBundle = {\n pane,\n update: () => graph?.update(),\n get disposed() {\n return disposed\n },\n }\n _unclaimedPane = bundle\n return bundle\n}\n"],"mappings":"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","names":[]}