@three-flatland/devtools 0.1.0-alpha.3 → 1.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/buffers-modal.d.ts +16 -52
- package/dist/buffers-modal.d.ts.map +1 -0
- package/dist/buffers-modal.js +459 -464
- package/dist/buffers-modal.js.map +1 -1
- package/dist/buffers-view.d.ts +22 -34
- package/dist/buffers-view.d.ts.map +1 -0
- package/dist/buffers-view.js +252 -240
- package/dist/buffers-view.js.map +1 -1
- package/dist/create-pane.d.ts +38 -37
- package/dist/create-pane.d.ts.map +1 -0
- package/dist/create-pane.js +197 -166
- package/dist/create-pane.js.map +1 -1
- package/dist/dashboard/app.tsx +2 -0
- package/dist/dashboard/export.ts +1 -3
- package/dist/dashboard/frame-cursor.ts +81 -0
- package/dist/dashboard/hooks.ts +10 -2
- package/dist/dashboard/index.html +61 -0
- package/dist/dashboard/panels/batches.tsx +5 -18
- package/dist/dashboard/panels/buffers.tsx +52 -26
- package/dist/dashboard/panels/env.tsx +5 -6
- package/dist/dashboard/panels/protocol-log.tsx +146 -58
- package/dist/dashboard/panels/registry.tsx +55 -69
- package/dist/dashboard/panels/scrubber.tsx +100 -0
- package/dist/dashboard/panels/stats.tsx +60 -32
- package/dist/dashboard/protocol-store.ts +25 -8
- package/dist/dashboard/vendor/hooks.module.d.ts +146 -0
- package/dist/dashboard/vendor/jsx-runtime.d.ts +55 -0
- package/dist/dashboard/vendor/jsx.d.ts +3123 -0
- package/dist/dashboard/vendor/preact.module.d.ts +397 -0
- package/dist/devtools-client.d.ts +312 -300
- package/dist/devtools-client.d.ts.map +1 -0
- package/dist/devtools-client.js +866 -838
- package/dist/devtools-client.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.js +5 -12
- package/dist/perf-trace.d.ts +6 -4
- package/dist/perf-trace.d.ts.map +1 -0
- package/dist/perf-trace.js +80 -66
- package/dist/perf-trace.js.map +1 -1
- package/dist/plugins.d.ts +6 -6
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +5 -5
- package/dist/plugins.js.map +1 -1
- package/dist/provider-switcher.d.ts +8 -15
- package/dist/provider-switcher.d.ts.map +1 -0
- package/dist/provider-switcher.js +68 -68
- package/dist/provider-switcher.js.map +1 -1
- package/dist/react/devtools-provider.d.ts +11 -10
- package/dist/react/devtools-provider.d.ts.map +1 -0
- package/dist/react/devtools-provider.js +60 -32
- package/dist/react/devtools-provider.js.map +1 -1
- package/dist/react/use-fps-graph.d.ts +7 -7
- package/dist/react/use-fps-graph.d.ts.map +1 -0
- package/dist/react/use-fps-graph.js +27 -21
- package/dist/react/use-fps-graph.js.map +1 -1
- package/dist/react/use-pane-button.d.ts +4 -4
- package/dist/react/use-pane-button.d.ts.map +1 -0
- package/dist/react/use-pane-button.js +35 -30
- package/dist/react/use-pane-button.js.map +1 -1
- package/dist/react/use-pane-folder.d.ts +5 -4
- package/dist/react/use-pane-folder.d.ts.map +1 -0
- package/dist/react/use-pane-folder.js +33 -20
- package/dist/react/use-pane-folder.js.map +1 -1
- package/dist/react/use-pane-input.d.ts +31 -30
- package/dist/react/use-pane-input.d.ts.map +1 -0
- package/dist/react/use-pane-input.js +46 -42
- package/dist/react/use-pane-input.js.map +1 -1
- package/dist/react/use-pane-radio-grid.d.ts +20 -20
- package/dist/react/use-pane-radio-grid.d.ts.map +1 -0
- package/dist/react/use-pane-radio-grid.js +72 -50
- package/dist/react/use-pane-radio-grid.js.map +1 -1
- package/dist/react/use-pane.d.ts +4 -4
- package/dist/react/use-pane.d.ts.map +1 -0
- package/dist/react/use-pane.js +52 -17
- package/dist/react/use-pane.js.map +1 -1
- package/dist/react.d.ts +10 -12
- package/dist/react.js +2 -12
- package/dist/registry-view.d.ts +8 -21
- package/dist/registry-view.d.ts.map +1 -0
- package/dist/registry-view.js +357 -338
- package/dist/registry-view.js.map +1 -1
- package/dist/relay.d.ts +16 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +189 -0
- package/dist/relay.js.map +1 -0
- package/dist/remote.d.ts +10 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +28 -0
- package/dist/remote.js.map +1 -0
- package/dist/stats-graph.d.ts +23 -31
- package/dist/stats-graph.d.ts.map +1 -0
- package/dist/stats-graph.js +323 -264
- package/dist/stats-graph.js.map +1 -1
- package/dist/stats-row.d.ts +8 -21
- package/dist/stats-row.d.ts.map +1 -0
- package/dist/stats-row.js +99 -101
- package/dist/stats-row.js.map +1 -1
- package/dist/theme.d.ts +3 -1
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +43 -54
- package/dist/theme.js.map +1 -1
- package/dist/vite-plugin.d.ts +7 -6
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +87 -78
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +33 -29
- package/dist/buffers-modal.cjs +0 -491
- package/dist/buffers-modal.cjs.map +0 -1
- package/dist/buffers-modal.d.cts +0 -62
- package/dist/buffers-view.cjs +0 -269
- package/dist/buffers-view.cjs.map +0 -1
- package/dist/buffers-view.d.cts +0 -39
- package/dist/create-pane.cjs +0 -203
- package/dist/create-pane.cjs.map +0 -1
- package/dist/create-pane.d.cts +0 -66
- package/dist/devtools-client.cjs +0 -870
- package/dist/devtools-client.cjs.map +0 -1
- package/dist/devtools-client.d.cts +0 -362
- package/dist/index.cjs +0 -42
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -8
- package/dist/index.js.map +0 -1
- package/dist/perf-trace.cjs +0 -103
- package/dist/perf-trace.cjs.map +0 -1
- package/dist/perf-trace.d.cts +0 -27
- package/dist/plugins.cjs +0 -44
- package/dist/plugins.cjs.map +0 -1
- package/dist/plugins.d.cts +0 -7
- package/dist/provider-switcher.cjs +0 -95
- package/dist/provider-switcher.cjs.map +0 -1
- package/dist/provider-switcher.d.cts +0 -18
- package/dist/react/devtools-provider.cjs +0 -59
- package/dist/react/devtools-provider.cjs.map +0 -1
- package/dist/react/devtools-provider.d.cts +0 -39
- package/dist/react/use-fps-graph.cjs +0 -49
- package/dist/react/use-fps-graph.cjs.map +0 -1
- package/dist/react/use-fps-graph.d.cts +0 -15
- package/dist/react/use-pane-button.cjs +0 -58
- package/dist/react/use-pane-button.cjs.map +0 -1
- package/dist/react/use-pane-button.d.cts +0 -10
- package/dist/react/use-pane-folder.cjs +0 -48
- package/dist/react/use-pane-folder.cjs.map +0 -1
- package/dist/react/use-pane-folder.d.cts +0 -15
- package/dist/react/use-pane-input.cjs +0 -70
- package/dist/react/use-pane-input.cjs.map +0 -1
- package/dist/react/use-pane-input.d.cts +0 -44
- package/dist/react/use-pane-radio-grid.cjs +0 -78
- package/dist/react/use-pane-radio-grid.cjs.map +0 -1
- package/dist/react/use-pane-radio-grid.d.cts +0 -35
- package/dist/react/use-pane.cjs +0 -45
- package/dist/react/use-pane.cjs.map +0 -1
- package/dist/react/use-pane.d.cts +0 -40
- package/dist/react.cjs +0 -50
- package/dist/react.cjs.map +0 -1
- package/dist/react.d.cts +0 -12
- package/dist/react.js.map +0 -1
- package/dist/registry-view.cjs +0 -371
- package/dist/registry-view.cjs.map +0 -1
- package/dist/registry-view.d.cts +0 -24
- package/dist/stats-graph.cjs +0 -297
- package/dist/stats-graph.cjs.map +0 -1
- package/dist/stats-graph.d.cts +0 -40
- package/dist/stats-row.cjs +0 -153
- package/dist/stats-row.cjs.map +0 -1
- package/dist/stats-row.d.cts +0 -24
- package/dist/theme.cjs +0 -260
- package/dist/theme.cjs.map +0 -1
- package/dist/theme.d.cts +0 -10
- package/dist/vite-plugin.cjs +0 -113
- package/dist/vite-plugin.cjs.map +0 -1
- package/dist/vite-plugin.d.cts +0 -9
package/dist/stats-graph.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stats-graph.ts"],"sourcesContent":["/**\n * Cycling stats graph — click to cycle FPS / MS / MEM (like stats.js).\n *\n * Replicates the exact DOM structure of Tweakpane's built-in fpsgraph blade.\n * Uses SVG polyline like GraphLogView (tp-grlv). Click anywhere to cycle modes.\n * Only the graph line is colorized per mode; text uses default Tweakpane colors.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\nimport { STATS_BATCH_MS } from 'three-flatland/debug-protocol'\n\nimport type { DevtoolsClient, DevtoolsSeries } from './devtools-client.js'\n\nexport interface StatsGraphHandle {\n /** The root element */\n readonly element: HTMLElement\n /**\n * Render a frame. Required when constructed with `driver: 'manual'`;\n * no-op for the `'raf'` driver (the internal rAF is doing it).\n */\n update(): void\n /** Stop and remove. */\n dispose(): void\n}\n\nexport interface AddStatsGraphOptions {\n rows?: number\n /**\n * `'raf'` (default) — the graph runs its own `requestAnimationFrame`\n * loop. `'manual'` — no rAF; caller drives via `handle.update()` from\n * their own frame tick (e.g. `renderer.setAnimationLoop`, R3F\n * `useFrame`).\n */\n driver?: 'raf' | 'manual'\n}\n\ntype Mode = 'fps' | 'ms' | 'gpu' | 'mem'\n\n// Cycle order: FPS → MS → GPU → MEM (gpu next to ms so you can A/B them).\nconst MODES: Mode[] = ['fps', 'ms', 'gpu', 'mem']\nconst BUFFER_SIZE = 80\n\n// Retro-themed line colors (only applied to the graph line)\nconst MODE_COLORS: Record<Mode, string> = {\n fps: '#47cca9', // retro-cyan\n ms: '#47cc6a', // green variant\n gpu: '#ffa347', // retro-amber\n mem: '#d94c87', // retro-pink\n}\n\n// Unit labels shown next to the live value overlay (bottom-right of the\n// graph). The left-side mode label distinguishes fps/ms/gpu/mem; this is\n// just the unit of the number itself, so `gpu` (also milliseconds) reads\n// `MS` here.\nconst MODE_UNITS: Record<Mode, string> = {\n fps: 'FPS',\n ms: 'MS',\n gpu: 'MS',\n mem: 'MB',\n}\n\n/**\n * Add a cycling stats graph to a Tweakpane parent.\n * Matches the visual design of the built-in fpsgraph blade.\n */\nexport function addStatsGraph(\n parent: Pane | FolderApi,\n client: DevtoolsClient,\n options: AddStatsGraphOptions = {},\n): StatsGraphHandle {\n const { rows = 2, driver = 'raf' } = options\n\n // State\n let mode: Mode = 'fps'\n let modeIndex = 0\n\n // Graph max scales\n // FPS: 120 (tops out at 120fps)\n // MS: 16.67 (one frame at 60fps — hitting the top means blown frame budget)\n // GPU: 16.67 (same frame budget as MS, just measured on the GPU side)\n // MEM: heap limit in MB — pulled from `state.heapLimitMB` once it\n // arrives; default 256 until then.\n const graphMax: Record<Mode, number> = {\n fps: 120,\n ms: 16.67,\n gpu: 16.67,\n mem: 256,\n }\n\n // Session min/max tracking (like stats.js displays \"60 FPS (55-62)\")\n const sessionMin: Record<Mode, number> = { fps: Infinity, ms: Infinity, gpu: Infinity, mem: Infinity }\n const sessionMax: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n\n // ── Build DOM matching Tweakpane's label + graph structure ──\n\n // tp-lblv: label view (left label + right value)\n const lblv = document.createElement('div')\n lblv.className = 'tp-lblv'\n lblv.style.cursor = 'pointer'\n\n const lblvL = document.createElement('div')\n lblvL.className = 'tp-lblv_l'\n lblvL.textContent = 'fps'\n lblv.appendChild(lblvL)\n\n // Value area (right side — full width when no label)\n const lblvV = document.createElement('div')\n lblvV.className = 'tp-lblv_v'\n lblv.appendChild(lblvV)\n\n // tp-fpsv: FPS view container (positions the overlay label)\n const fpsv = document.createElement('div')\n fpsv.className = 'tp-fpsv'\n lblvV.appendChild(fpsv)\n\n // tp-fpsv_g: graph area\n const fpsvG = document.createElement('div')\n fpsvG.className = 'tp-fpsv_g'\n fpsv.appendChild(fpsvG)\n\n // tp-grlv: graph log view. We render into a 2D canvas instead of an\n // SVG polyline because per-frame `setAttribute('points', ...)` was\n // (a) allocating long template-literal strings every rAF and (b)\n // forcing the browser to re-allocate CSS-selector strings to\n // re-evaluate styles for every ancestor in the `.tp-cntv .tp-v-fst …`\n // chain — a measurable string-GC cost in profiles. Canvas drawing is\n // pure path commands, no DOM mutation.\n const grlv = document.createElement('div')\n grlv.className = 'tp-grlv'\n fpsvG.appendChild(grlv)\n\n const canvas = document.createElement('canvas')\n canvas.classList.add('tp-grlv_g')\n canvas.style.cssText = `display:block;width:100%;height:calc(var(--cnt-usz, 20px) * ${rows})`\n canvas.width = 1\n canvas.height = 1\n grlv.appendChild(canvas)\n const gfx = canvas.getContext('2d')\n\n // tp-fpsv_l: value label overlay (bottom-right) with bg for readability\n const fpsvL = document.createElement('div')\n fpsvL.className = 'tp-fpsv_l'\n fpsvL.style.cssText = 'background:rgba(0,2,28,0.65);padding:1px 4px;border-radius:2px;'\n fpsv.appendChild(fpsvL)\n\n const valueSpan = document.createElement('span')\n valueSpan.className = 'tp-fpsv_v'\n valueSpan.textContent = '--'\n fpsvL.appendChild(valueSpan)\n\n const unitSpan = document.createElement('span')\n unitSpan.className = 'tp-fpsv_u'\n unitSpan.textContent = 'FPS'\n fpsvL.appendChild(unitSpan)\n\n // ── Inject into Tweakpane via a real blade slot ──\n // Use addBlade to get a proper slot in the blade rack (maintains ordering).\n // Then replace the blade element's inner content with our custom graph.\n\n const blade = parent.addBlade({ view: 'separator' })\n const bladeEl = blade.element\n\n // Replace separator content with our graph layout\n bladeEl.innerHTML = ''\n bladeEl.className = 'tp-cntv'\n bladeEl.style.cssText = ''\n bladeEl.appendChild(lblv)\n\n // ── Mode cycling ──\n\n function applyMode() {\n lblvL.textContent = mode\n unitSpan.textContent = MODE_UNITS[mode]\n }\n applyMode()\n\n lblv.addEventListener('click', () => {\n // Only cycle to modes the producer is actually streaming values for\n // (gpuMs for gpu, heapUsedMB for mem). Client state is the source of\n // truth — no separate enabled flags.\n const state = client.state\n do {\n modeIndex = (modeIndex + 1) % MODES.length\n mode = MODES[modeIndex]!\n } while (\n (mode === 'mem' && state.heapUsedMB === undefined) ||\n (mode === 'gpu' && !state.gpuModeEnabled)\n )\n applyMode()\n })\n\n // ── Graph rendering ──\n\n // Sync canvas backing to CSS box × DPR so 1 source pixel = 1 device\n // pixel; avoids per-frame `getBoundingClientRect` which Safari pays\n // dearly for, and keeps the polyline crisp on HiDPI.\n let cachedW = 0\n let cachedH = 0\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return\n const r = entry.contentRect\n const dpr = window.devicePixelRatio || 1\n const w = Math.max(1, Math.round(r.width * dpr))\n const h = Math.max(1, Math.round(r.height * dpr))\n if (canvas.width !== w || canvas.height !== h) {\n canvas.width = w\n canvas.height = h\n }\n cachedW = w\n cachedH = h\n })\n resizeObserver.observe(canvas)\n\n const MODE_LABEL: Record<Mode, string> = { fps: 'fps', ms: 'ms', gpu: 'gpu', mem: 'mem' }\n const MODE_VALUE_UNIT: Record<Mode, string> = { fps: 'FPS', ms: 'MS', gpu: 'MS', mem: 'MB' }\n const MODE_RANGE_UNIT: Record<Mode, string> = { fps: 'FPS', ms: 'MS', gpu: 'GPU', mem: 'MB' }\n const MODE_DECIMALS: Record<Mode, number> = { fps: 0, ms: 1, gpu: 1, mem: 0 }\n const MODE_SCALE: Record<Mode, number> = { fps: 1, ms: 10, gpu: 10, mem: 1 }\n\n // Rounded-integer-keyed cache for the `toFixed` result per mode.\n // `toFixed` allocates a fresh string every call; with this cache,\n // we only allocate when the rounded display value actually changes\n // (e.g. fps stays at 60 for many rAF ticks while the lerped value\n // wiggles in the high 59s — single string reused for the whole run).\n const fmtCache: Record<Mode, { key: number; str: string }> = {\n fps: { key: NaN, str: '' },\n ms: { key: NaN, str: '' },\n gpu: { key: NaN, str: '' },\n mem: { key: NaN, str: '' },\n }\n function fmtCached(m: Mode, v: number): string {\n const key = Math.round(v * MODE_SCALE[m])\n const c = fmtCache[m]\n if (c.key === key) return c.str\n c.key = key\n c.str = v.toFixed(MODE_DECIMALS[m])\n return c.str\n }\n\n function seriesFor(m: Mode): DevtoolsSeries {\n const s = client.state.series\n switch (m) {\n case 'fps': return s.fps\n case 'ms': return s.cpuMs\n case 'gpu': return s.gpuMs\n case 'mem': return s.heapUsedMB\n }\n }\n\n // ── Arrival snapshots (written in the listener, read in RAF) ──────────\n // The listener only snapshots values and resets the interpolation\n // clock. RAF does all rendering, so the display stays smooth between\n // the 4 Hz batches.\n const prevLabel: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n const currLabel: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n const hasLabel: Record<Mode, boolean> = { fps: false, ms: false, gpu: false, mem: false }\n let batchCount = 0\n let lastBatchAt = performance.now()\n let prevRingWrite = 0\n\n const snap = (m: Mode, v: number | undefined): void => {\n if (v === undefined) return\n prevLabel[m] = hasLabel[m] ? currLabel[m] : v\n currLabel[m] = v\n hasLabel[m] = true\n }\n\n const unsubscribe = client.addListener((s) => {\n if (s.heapLimitMB !== undefined) graphMax.mem = s.heapLimitMB\n snap('fps', s.fps)\n snap('ms', s.cpuMs)\n snap('gpu', s.gpuMs)\n snap('mem', s.heapUsedMB)\n // How many new samples came in this batch? All series write in\n // lockstep, so we can just watch `fps`.\n const fps = s.series.fps\n const size = fps.data.length\n const delta = (fps.write - prevRingWrite + size) % size\n if (delta > 0) {\n batchCount = delta\n prevRingWrite = fps.write\n lastBatchAt = performance.now()\n }\n })\n\n // ── Render ────────────────────────────────────────────────────────────\n // Canvas path-based draw — no DOM attribute mutation per frame, no\n // string allocations beyond the unavoidable `toFixed` for the text\n // label (and even those are deduped: textContent is only re-assigned\n // when the rendered string actually changes).\n\n let rafId = 0\n let disposed = false\n\n function setText(node: HTMLElement, next: string, cache: { v: string }): void {\n if (cache.v === next) return\n node.textContent = next\n cache.v = next\n }\n // Boxed cache holders keep `setText` allocation-free past creation.\n const labelCache = { v: '' }\n const valueCache = { v: '' }\n const unitCache = { v: '' }\n const titleCache = { v: '' }\n\n function update(): void {\n if (disposed) return\n if (cachedW === 0 || cachedH === 0 || gfx === null) return\n\n const now = performance.now()\n const t = Math.min(1, Math.max(0, (now - lastBatchAt) / STATS_BATCH_MS))\n\n // Slide the display window through the ring so newest samples fade\n // in at 60 Hz between the producer's 4 Hz batches.\n //\n // **Don't blend sample VALUES across the slide.** Earlier the\n // sample at each pixel was `a*(1-frac) + b*frac` for a\n // continuously-changing `frac` — that means a single peak at\n // ring slot K shows different *heights* at different sub-batch\n // times (full height only when frac=0; lower whenever it's\n // blended with a neighbor). Visible as peaks \"changing shape as\n // they scroll.\" Sample each ring slot at its INTEGER index — the\n // height never moves — and shift the entire polyline's X by the\n // fractional sub-sample offset. The line slides smoothly across\n // the canvas; each sample's height is the same in every frame\n // that contains it.\n const ring = seriesFor(mode)\n const size = ring.data.length\n const max = graphMax[mode] || 1\n const phase = batchCount * (1 - t) // continuous 0..batchCount\n const intPhase = Math.floor(phase) // integer slide steps remaining\n const fracPhase = phase - intPhase // 0..1 sub-step (sub-pixel scroll)\n\n const w = cachedW\n const h = cachedH\n gfx.clearRect(0, 0, w, h)\n\n const color = MODE_COLORS[mode]\n const samplePixelW = w / (BUFFER_SIZE - 1)\n\n // Draw BUFFER_SIZE + 1 samples — one extra at i=-1 so the leftmost\n // visible pixel always has a polyline endpoint, no matter where\n // fracPhase sits between integer steps. Without this extra, the\n // line would have a visible gap at the canvas's left edge during\n // sub-pixel slide.\n //\n // Two passes (fill polygon, then crisp stroke) over the same\n // points; computing each point in a tight inner loop keeps allocs\n // at zero. The values cache `mn`/`mx` are session-min/max trackers,\n // populated from the ring (not the rendered polyline) — they\n // bypass the integer-index loop and read raw ring values.\n let mn = Infinity\n let mx = 0\n // Walk the same range we render to compute mn/mx — no separate\n // pass. Adds an `if` per sample but skips a second loop.\n const baseRingIdx = ring.write - intPhase - BUFFER_SIZE + size * 2\n\n gfx.beginPath()\n for (let i = -1; i < BUFFER_SIZE; i++) {\n const ringIdx = (baseRingIdx + i) % size\n const v = ring.data[ringIdx]!\n if (i >= 0) {\n if (v < mn) mn = v\n if (v > mx) mx = v\n }\n const x = (i + fracPhase) * samplePixelW\n // Clamp y to the canvas top so over-budget samples (v > graphMax)\n // saturate at the top edge instead of drawing above the canvas.\n const yRaw = h - (v / max) * h * 0.85 - h * 0.05\n const y = yRaw < h * 0.05 ? h * 0.05 : yRaw\n if (i === -1) gfx.moveTo(x, y)\n else gfx.lineTo(x, y)\n }\n\n // Translucent fill under the line. Close to bottom-right then\n // bottom-left of the canvas — the polyline's leftmost x may be\n // slightly negative during sub-pixel slide, but the canvas crops\n // off-edge pixels for free, so the polygon closes cleanly.\n gfx.lineTo(w, h)\n gfx.lineTo(0, h)\n gfx.closePath()\n gfx.fillStyle = color\n gfx.globalAlpha = 0.33\n gfx.fill()\n gfx.globalAlpha = 1\n\n // Re-stroke the line over the fill so the top edge is sharp.\n gfx.beginPath()\n for (let i = -1; i < BUFFER_SIZE; i++) {\n const ringIdx = (baseRingIdx + i) % size\n const v = ring.data[ringIdx]!\n const x = (i + fracPhase) * samplePixelW\n const yRaw = h - (v / max) * h * 0.85 - h * 0.05\n const y = yRaw < h * 0.05 ? h * 0.05 : yRaw\n if (i === -1) gfx.moveTo(x, y)\n else gfx.lineTo(x, y)\n }\n gfx.strokeStyle = color\n gfx.lineWidth = Math.max(1, (window.devicePixelRatio || 1))\n gfx.stroke()\n\n if (hasLabel[mode]) {\n sessionMin[mode] = Math.min(sessionMin[mode], mn)\n sessionMax[mode] = Math.max(sessionMax[mode], mx)\n }\n\n // Text — lerp the label's number toward the latest batch mean.\n // `toFixed` allocates a new string each call (~one per rAF) but\n // we dedupe assignment to avoid setting `textContent` redundantly,\n // which would trigger DOM mutation work even when the string is\n // identical.\n const has = hasLabel[mode]\n const lerped = prevLabel[mode] + (currLabel[mode] - prevLabel[mode]) * t\n const sMn = sessionMin[mode]\n const sMx = sessionMax[mode]\n setText(lblvL, MODE_LABEL[mode], labelCache)\n setText(valueSpan, has ? fmtCached(mode, lerped) : '--', valueCache)\n setText(unitSpan, MODE_VALUE_UNIT[mode], unitCache)\n const nextTitle = sMn !== Infinity ? `${MODE_RANGE_UNIT[mode]} range: ${Math.round(sMn)}–${Math.round(sMx)}` : ''\n if (titleCache.v !== nextTitle) {\n lblvL.title = nextTitle\n titleCache.v = nextTitle\n }\n }\n\n if (driver === 'raf') {\n const autoFrame = (): void => {\n if (disposed) return\n rafId = requestAnimationFrame(autoFrame)\n update()\n }\n rafId = requestAnimationFrame(autoFrame)\n }\n\n return {\n element: bladeEl,\n // In `'raf'` mode the internal loop already runs, so public `update()`\n // is a no-op — we don't want to paint twice per frame when the host\n // also (redundantly) drives it. In `'manual'` mode this is the only\n // way the graph gets painted.\n update: driver === 'manual' ? update : () => {},\n dispose() {\n disposed = true\n if (rafId !== 0) cancelAnimationFrame(rafId)\n unsubscribe()\n resizeObserver.disconnect()\n blade.dispose()\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"stats-graph.js","names":[],"sources":["../src/stats-graph.ts"],"sourcesContent":["/**\n * Cycling stats graph — click to cycle FPS / MS / MEM (like stats.js).\n *\n * Replicates the exact DOM structure of Tweakpane's built-in fpsgraph blade.\n * Uses SVG polyline like GraphLogView (tp-grlv). Click anywhere to cycle modes.\n * Only the graph line is colorized per mode; text uses default Tweakpane colors.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\nimport { STATS_BATCH_MS } from 'three-flatland/debug-protocol'\n\nimport type { DevtoolsClient, DevtoolsSeries } from './devtools-client.js'\n\nexport interface StatsGraphHandle {\n /** The root element */\n readonly element: HTMLElement\n /**\n * Render a frame. Required when constructed with `driver: 'manual'`;\n * no-op for the `'raf'` driver (the internal rAF is doing it).\n */\n update(): void\n /** Stop and remove. */\n dispose(): void\n}\n\nexport interface AddStatsGraphOptions {\n rows?: number\n /**\n * `'raf'` (default) — the graph runs its own `requestAnimationFrame`\n * loop. `'manual'` — no rAF; caller drives via `handle.update()` from\n * their own frame tick (e.g. `renderer.setAnimationLoop`, R3F\n * `useFrame`).\n */\n driver?: 'raf' | 'manual'\n}\n\ntype Mode = 'fps' | 'ms' | 'gpu' | 'mem'\n\n// Cycle order: FPS → MS → GPU → MEM (gpu next to ms so you can A/B them).\nconst MODES: Mode[] = ['fps', 'ms', 'gpu', 'mem']\nconst BUFFER_SIZE = 80\n\n// Retro-themed line colors (only applied to the graph line)\nconst MODE_COLORS: Record<Mode, string> = {\n fps: '#47cca9', // retro-cyan\n ms: '#47cc6a', // green variant\n gpu: '#ffa347', // retro-amber\n mem: '#d94c87', // retro-pink\n}\n\n// Unit labels shown next to the live value overlay (bottom-right of the\n// graph). The left-side mode label distinguishes fps/ms/gpu/mem; this is\n// just the unit of the number itself, so `gpu` (also milliseconds) reads\n// `MS` here.\nconst MODE_UNITS: Record<Mode, string> = {\n fps: 'FPS',\n ms: 'MS',\n gpu: 'MS',\n mem: 'MB',\n}\n\n/**\n * Add a cycling stats graph to a Tweakpane parent.\n * Matches the visual design of the built-in fpsgraph blade.\n */\nexport function addStatsGraph(\n parent: Pane | FolderApi,\n client: DevtoolsClient,\n options: AddStatsGraphOptions = {}\n): StatsGraphHandle {\n const { rows = 2, driver = 'raf' } = options\n\n // State\n let mode: Mode = 'fps'\n let modeIndex = 0\n\n // Graph max scales\n // FPS: 120 (tops out at 120fps)\n // MS: 16.67 (one frame at 60fps — hitting the top means blown frame budget)\n // GPU: 16.67 (same frame budget as MS, just measured on the GPU side)\n // MEM: heap limit in MB — pulled from `state.heapLimitMB` once it\n // arrives; default 256 until then.\n const graphMax: Record<Mode, number> = {\n fps: 120,\n ms: 16.67,\n gpu: 16.67,\n mem: 256,\n }\n\n // Session min/max tracking (like stats.js displays \"60 FPS (55-62)\")\n const sessionMin: Record<Mode, number> = { fps: Infinity, ms: Infinity, gpu: Infinity, mem: Infinity }\n const sessionMax: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n\n // ── Build DOM matching Tweakpane's label + graph structure ──\n\n // tp-lblv: label view (left label + right value)\n const lblv = document.createElement('div')\n lblv.className = 'tp-lblv'\n lblv.style.cursor = 'pointer'\n\n const lblvL = document.createElement('div')\n lblvL.className = 'tp-lblv_l'\n lblvL.textContent = 'fps'\n lblv.appendChild(lblvL)\n\n // Value area (right side — full width when no label)\n const lblvV = document.createElement('div')\n lblvV.className = 'tp-lblv_v'\n lblv.appendChild(lblvV)\n\n // tp-fpsv: FPS view container (positions the overlay label)\n const fpsv = document.createElement('div')\n fpsv.className = 'tp-fpsv'\n lblvV.appendChild(fpsv)\n\n // tp-fpsv_g: graph area\n const fpsvG = document.createElement('div')\n fpsvG.className = 'tp-fpsv_g'\n fpsv.appendChild(fpsvG)\n\n // tp-grlv: graph log view. We render into a 2D canvas instead of an\n // SVG polyline because per-frame `setAttribute('points', ...)` was\n // (a) allocating long template-literal strings every rAF and (b)\n // forcing the browser to re-allocate CSS-selector strings to\n // re-evaluate styles for every ancestor in the `.tp-cntv .tp-v-fst …`\n // chain — a measurable string-GC cost in profiles. Canvas drawing is\n // pure path commands, no DOM mutation.\n const grlv = document.createElement('div')\n grlv.className = 'tp-grlv'\n fpsvG.appendChild(grlv)\n\n const canvas = document.createElement('canvas')\n canvas.classList.add('tp-grlv_g')\n canvas.style.cssText = `display:block;width:100%;height:calc(var(--cnt-usz, 20px) * ${rows})`\n canvas.width = 1\n canvas.height = 1\n grlv.appendChild(canvas)\n const gfx = canvas.getContext('2d')\n\n // tp-fpsv_l: value label overlay (bottom-right) with bg for readability\n const fpsvL = document.createElement('div')\n fpsvL.className = 'tp-fpsv_l'\n fpsvL.style.cssText = 'background:rgba(0,2,28,0.65);padding:1px 4px;border-radius:2px;'\n fpsv.appendChild(fpsvL)\n\n const valueSpan = document.createElement('span')\n valueSpan.className = 'tp-fpsv_v'\n valueSpan.textContent = '--'\n fpsvL.appendChild(valueSpan)\n\n const unitSpan = document.createElement('span')\n unitSpan.className = 'tp-fpsv_u'\n unitSpan.textContent = 'FPS'\n fpsvL.appendChild(unitSpan)\n\n // ── Inject into Tweakpane via a real blade slot ──\n // Use addBlade to get a proper slot in the blade rack (maintains ordering).\n // Then replace the blade element's inner content with our custom graph.\n\n const blade = parent.addBlade({ view: 'separator' })\n const bladeEl = blade.element\n\n // Replace separator content with our graph layout\n bladeEl.innerHTML = ''\n bladeEl.className = 'tp-cntv'\n bladeEl.style.cssText = ''\n bladeEl.appendChild(lblv)\n\n // ── Mode cycling ──\n\n function applyMode() {\n lblvL.textContent = mode\n unitSpan.textContent = MODE_UNITS[mode]\n }\n applyMode()\n\n lblv.addEventListener('click', () => {\n // Only cycle to modes the producer is actually streaming values for\n // (gpuMs for gpu, heapUsedMB for mem). Client state is the source of\n // truth — no separate enabled flags.\n const state = client.state\n do {\n modeIndex = (modeIndex + 1) % MODES.length\n mode = MODES[modeIndex]!\n } while ((mode === 'mem' && state.heapUsedMB === undefined) || (mode === 'gpu' && !state.gpuModeEnabled))\n applyMode()\n })\n\n // ── Graph rendering ──\n\n // Sync canvas backing to CSS box × DPR so 1 source pixel = 1 device\n // pixel; avoids per-frame `getBoundingClientRect` which Safari pays\n // dearly for, and keeps the polyline crisp on HiDPI.\n let cachedW = 0\n let cachedH = 0\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return\n const r = entry.contentRect\n const dpr = window.devicePixelRatio || 1\n const w = Math.max(1, Math.round(r.width * dpr))\n const h = Math.max(1, Math.round(r.height * dpr))\n if (canvas.width !== w || canvas.height !== h) {\n canvas.width = w\n canvas.height = h\n }\n cachedW = w\n cachedH = h\n })\n resizeObserver.observe(canvas)\n\n const MODE_LABEL: Record<Mode, string> = { fps: 'fps', ms: 'ms', gpu: 'gpu', mem: 'mem' }\n const MODE_VALUE_UNIT: Record<Mode, string> = { fps: 'FPS', ms: 'MS', gpu: 'MS', mem: 'MB' }\n const MODE_RANGE_UNIT: Record<Mode, string> = { fps: 'FPS', ms: 'MS', gpu: 'GPU', mem: 'MB' }\n const MODE_DECIMALS: Record<Mode, number> = { fps: 0, ms: 1, gpu: 1, mem: 0 }\n const MODE_SCALE: Record<Mode, number> = { fps: 1, ms: 10, gpu: 10, mem: 1 }\n\n // Rounded-integer-keyed cache for the `toFixed` result per mode.\n // `toFixed` allocates a fresh string every call; with this cache,\n // we only allocate when the rounded display value actually changes\n // (e.g. fps stays at 60 for many rAF ticks while the lerped value\n // wiggles in the high 59s — single string reused for the whole run).\n const fmtCache: Record<Mode, { key: number; str: string }> = {\n fps: { key: NaN, str: '' },\n ms: { key: NaN, str: '' },\n gpu: { key: NaN, str: '' },\n mem: { key: NaN, str: '' },\n }\n function fmtCached(m: Mode, v: number): string {\n const key = Math.round(v * MODE_SCALE[m])\n const c = fmtCache[m]\n if (c.key === key) return c.str\n c.key = key\n c.str = v.toFixed(MODE_DECIMALS[m])\n return c.str\n }\n\n function seriesFor(m: Mode): DevtoolsSeries {\n const s = client.state.series\n switch (m) {\n case 'fps':\n return s.fps\n case 'ms':\n return s.cpuMs\n case 'gpu':\n return s.gpuMs\n case 'mem':\n return s.heapUsedMB\n }\n }\n\n // ── Arrival snapshots (written in the listener, read in RAF) ──────────\n // The listener only snapshots values and resets the interpolation\n // clock. RAF does all rendering, so the display stays smooth between\n // the 4 Hz batches.\n const prevLabel: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n const currLabel: Record<Mode, number> = { fps: 0, ms: 0, gpu: 0, mem: 0 }\n const hasLabel: Record<Mode, boolean> = { fps: false, ms: false, gpu: false, mem: false }\n let batchCount = 0\n let lastBatchAt = performance.now()\n let prevRingWrite = 0\n\n const snap = (m: Mode, v: number | undefined): void => {\n if (v === undefined) return\n prevLabel[m] = hasLabel[m] ? currLabel[m] : v\n currLabel[m] = v\n hasLabel[m] = true\n }\n\n const unsubscribe = client.addListener((s) => {\n if (s.heapLimitMB !== undefined) graphMax.mem = s.heapLimitMB\n snap('fps', s.fps)\n snap('ms', s.cpuMs)\n snap('gpu', s.gpuMs)\n snap('mem', s.heapUsedMB)\n // How many new samples came in this batch? All series write in\n // lockstep, so we can just watch `fps`.\n const fps = s.series.fps\n const size = fps.data.length\n const delta = (fps.write - prevRingWrite + size) % size\n if (delta > 0) {\n batchCount = delta\n prevRingWrite = fps.write\n lastBatchAt = performance.now()\n }\n })\n\n // ── Render ────────────────────────────────────────────────────────────\n // Canvas path-based draw — no DOM attribute mutation per frame, no\n // string allocations beyond the unavoidable `toFixed` for the text\n // label (and even those are deduped: textContent is only re-assigned\n // when the rendered string actually changes).\n\n let rafId = 0\n let disposed = false\n\n function setText(node: HTMLElement, next: string, cache: { v: string }): void {\n if (cache.v === next) return\n node.textContent = next\n cache.v = next\n }\n // Boxed cache holders keep `setText` allocation-free past creation.\n const labelCache = { v: '' }\n const valueCache = { v: '' }\n const unitCache = { v: '' }\n const titleCache = { v: '' }\n\n function update(): void {\n if (disposed) return\n if (cachedW === 0 || cachedH === 0 || gfx === null) return\n\n const now = performance.now()\n const t = Math.min(1, Math.max(0, (now - lastBatchAt) / STATS_BATCH_MS))\n\n // Slide the display window through the ring so newest samples fade\n // in at 60 Hz between the producer's 4 Hz batches.\n //\n // **Don't blend sample VALUES across the slide.** Earlier the\n // sample at each pixel was `a*(1-frac) + b*frac` for a\n // continuously-changing `frac` — that means a single peak at\n // ring slot K shows different *heights* at different sub-batch\n // times (full height only when frac=0; lower whenever it's\n // blended with a neighbor). Visible as peaks \"changing shape as\n // they scroll.\" Sample each ring slot at its INTEGER index — the\n // height never moves — and shift the entire polyline's X by the\n // fractional sub-sample offset. The line slides smoothly across\n // the canvas; each sample's height is the same in every frame\n // that contains it.\n const ring = seriesFor(mode)\n const size = ring.data.length\n const max = graphMax[mode] || 1\n const phase = batchCount * (1 - t) // continuous 0..batchCount\n const intPhase = Math.floor(phase) // integer slide steps remaining\n const fracPhase = phase - intPhase // 0..1 sub-step (sub-pixel scroll)\n\n const w = cachedW\n const h = cachedH\n gfx.clearRect(0, 0, w, h)\n\n const color = MODE_COLORS[mode]\n const samplePixelW = w / (BUFFER_SIZE - 1)\n\n // Draw BUFFER_SIZE + 1 samples — one extra at i=-1 so the leftmost\n // visible pixel always has a polyline endpoint, no matter where\n // fracPhase sits between integer steps. Without this extra, the\n // line would have a visible gap at the canvas's left edge during\n // sub-pixel slide.\n //\n // Two passes (fill polygon, then crisp stroke) over the same\n // points; computing each point in a tight inner loop keeps allocs\n // at zero. The values cache `mn`/`mx` are session-min/max trackers,\n // populated from the ring (not the rendered polyline) — they\n // bypass the integer-index loop and read raw ring values.\n let mn = Infinity\n let mx = 0\n // Walk the same range we render to compute mn/mx — no separate\n // pass. Adds an `if` per sample but skips a second loop.\n const baseRingIdx = ring.write - intPhase - BUFFER_SIZE + size * 2\n\n gfx.beginPath()\n for (let i = -1; i < BUFFER_SIZE; i++) {\n const ringIdx = (baseRingIdx + i) % size\n const v = ring.data[ringIdx]!\n if (i >= 0) {\n if (v < mn) mn = v\n if (v > mx) mx = v\n }\n const x = (i + fracPhase) * samplePixelW\n // Clamp y to the canvas top so over-budget samples (v > graphMax)\n // saturate at the top edge instead of drawing above the canvas.\n const yRaw = h - (v / max) * h * 0.85 - h * 0.05\n const y = yRaw < h * 0.05 ? h * 0.05 : yRaw\n if (i === -1) gfx.moveTo(x, y)\n else gfx.lineTo(x, y)\n }\n\n // Translucent fill under the line. Close to bottom-right then\n // bottom-left of the canvas — the polyline's leftmost x may be\n // slightly negative during sub-pixel slide, but the canvas crops\n // off-edge pixels for free, so the polygon closes cleanly.\n gfx.lineTo(w, h)\n gfx.lineTo(0, h)\n gfx.closePath()\n gfx.fillStyle = color\n gfx.globalAlpha = 0.33\n gfx.fill()\n gfx.globalAlpha = 1\n\n // Re-stroke the line over the fill so the top edge is sharp.\n gfx.beginPath()\n for (let i = -1; i < BUFFER_SIZE; i++) {\n const ringIdx = (baseRingIdx + i) % size\n const v = ring.data[ringIdx]!\n const x = (i + fracPhase) * samplePixelW\n const yRaw = h - (v / max) * h * 0.85 - h * 0.05\n const y = yRaw < h * 0.05 ? h * 0.05 : yRaw\n if (i === -1) gfx.moveTo(x, y)\n else gfx.lineTo(x, y)\n }\n gfx.strokeStyle = color\n gfx.lineWidth = Math.max(1, window.devicePixelRatio || 1)\n gfx.stroke()\n\n if (hasLabel[mode]) {\n sessionMin[mode] = Math.min(sessionMin[mode], mn)\n sessionMax[mode] = Math.max(sessionMax[mode], mx)\n }\n\n // Text — lerp the label's number toward the latest batch mean.\n // `toFixed` allocates a new string each call (~one per rAF) but\n // we dedupe assignment to avoid setting `textContent` redundantly,\n // which would trigger DOM mutation work even when the string is\n // identical.\n const has = hasLabel[mode]\n const lerped = prevLabel[mode] + (currLabel[mode] - prevLabel[mode]) * t\n const sMn = sessionMin[mode]\n const sMx = sessionMax[mode]\n setText(lblvL, MODE_LABEL[mode], labelCache)\n setText(valueSpan, has ? fmtCached(mode, lerped) : '--', valueCache)\n setText(unitSpan, MODE_VALUE_UNIT[mode], unitCache)\n const nextTitle = sMn !== Infinity ? `${MODE_RANGE_UNIT[mode]} range: ${Math.round(sMn)}–${Math.round(sMx)}` : ''\n if (titleCache.v !== nextTitle) {\n lblvL.title = nextTitle\n titleCache.v = nextTitle\n }\n }\n\n if (driver === 'raf') {\n const autoFrame = (): void => {\n if (disposed) return\n rafId = requestAnimationFrame(autoFrame)\n update()\n }\n rafId = requestAnimationFrame(autoFrame)\n }\n\n return {\n element: bladeEl,\n // In `'raf'` mode the internal loop already runs, so public `update()`\n // is a no-op — we don't want to paint twice per frame when the host\n // also (redundantly) drives it. In `'manual'` mode this is the only\n // way the graph gets painted.\n update: driver === 'manual' ? update : () => {},\n dispose() {\n disposed = true\n if (rafId !== 0) cancelAnimationFrame(rafId)\n unsubscribe()\n resizeObserver.disconnect()\n blade.dispose()\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/stats-row.d.ts
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Compact single-row renderer stats blade.
|
|
7
|
-
*
|
|
8
|
-
* Five equal-width cells separated by dim vertical dividers:
|
|
9
|
-
* `✎ 3 | △ 10K | ◇ 42 | ⌂ 12 | ▦ 3`
|
|
10
|
-
*
|
|
11
|
-
* Each cell shows an outline icon on the left and a left-aligned,
|
|
12
|
-
* compact-formatted number on the right. A native `title=` tooltip on each
|
|
13
|
-
* cell exposes the full field name. Injected via the same "replace a
|
|
14
|
-
* separator blade's content" trick that `stats-graph.ts` uses, so the blade
|
|
15
|
-
* still gets a proper slot in the blade rack.
|
|
16
|
-
*/
|
|
17
|
-
|
|
1
|
+
import { DevtoolsClient } from "./devtools-client.js";
|
|
2
|
+
import { FolderApi, Pane } from "tweakpane";
|
|
3
|
+
//#region src/stats-row.d.ts
|
|
18
4
|
interface StatsRowHandle {
|
|
19
|
-
|
|
20
|
-
|
|
5
|
+
readonly element: HTMLElement;
|
|
6
|
+
dispose(): void;
|
|
21
7
|
}
|
|
22
8
|
declare function addStatsRow(parent: Pane | FolderApi, client: DevtoolsClient): StatsRowHandle;
|
|
23
|
-
|
|
24
|
-
export {
|
|
9
|
+
//#endregion
|
|
10
|
+
export { StatsRowHandle, addStatsRow };
|
|
11
|
+
//# sourceMappingURL=stats-row.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats-row.d.ts","names":[],"sources":["../src/stats-row.ts"],"mappings":";;;UA0BiB;WACN,SAAS;EAClB;;iBAsGc,YAAY,QAAQ,OAAO,WAAW,QAAQ,iBAAiB"}
|
package/dist/stats-row.js
CHANGED
|
@@ -1,52 +1,50 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/stats-row.ts
|
|
2
|
+
const ICON_ATTRS = "viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block;width:calc(1em - 1px);height:calc(1em - 1px)\"";
|
|
2
3
|
const ICONS = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
prims: `<svg ${ICON_ATTRS}><path d="M1 1 L13 1 L13 13 L1 13 Z"/></svg>`,
|
|
9
|
-
// Regular-ish pentagon (dodecahedron silhouette), y-scaled to fill the box
|
|
10
|
-
geoms: `<svg ${ICON_ATTRS}><path d="M7 1 L13 5.6 L10.7 13 L3.3 13 L1 5.6 Z"/></svg>`,
|
|
11
|
-
// 3×3 dot grid (filled — swap stroke→fill). Centers spread so outer dot
|
|
12
|
-
// edges touch the same (1,1)–(13,13) box as the other icons.
|
|
13
|
-
textures: `<svg viewBox="0 0 14 14" fill="currentColor" style="display:block;width:calc(1em - 1px);height:calc(1em - 1px)"><circle cx="2.1" cy="2.1" r="1.1"/><circle cx="7" cy="2.1" r="1.1"/><circle cx="11.9" cy="2.1" r="1.1"/><circle cx="2.1" cy="7" r="1.1"/><circle cx="7" cy="7" r="1.1"/><circle cx="11.9" cy="7" r="1.1"/><circle cx="2.1" cy="11.9" r="1.1"/><circle cx="7" cy="11.9" r="1.1"/><circle cx="11.9" cy="11.9" r="1.1"/></svg>`
|
|
4
|
+
draws: `<svg ${ICON_ATTRS}><path d="M1 13 L3 13 L13 3 L11 1 L1 11 Z"/><path d="M9 3 L11 5"/></svg>`,
|
|
5
|
+
tris: `<svg ${ICON_ATTRS}><path d="M7 1 L13 13 L1 13 Z"/></svg>`,
|
|
6
|
+
prims: `<svg ${ICON_ATTRS}><path d="M1 1 L13 1 L13 13 L1 13 Z"/></svg>`,
|
|
7
|
+
geoms: `<svg ${ICON_ATTRS}><path d="M7 1 L13 5.6 L10.7 13 L3.3 13 L1 5.6 Z"/></svg>`,
|
|
8
|
+
textures: `<svg viewBox="0 0 14 14" fill="currentColor" style="display:block;width:calc(1em - 1px);height:calc(1em - 1px)"><circle cx="2.1" cy="2.1" r="1.1"/><circle cx="7" cy="2.1" r="1.1"/><circle cx="11.9" cy="2.1" r="1.1"/><circle cx="2.1" cy="7" r="1.1"/><circle cx="7" cy="7" r="1.1"/><circle cx="11.9" cy="7" r="1.1"/><circle cx="2.1" cy="11.9" r="1.1"/><circle cx="7" cy="11.9" r="1.1"/><circle cx="11.9" cy="11.9" r="1.1"/></svg>`
|
|
14
9
|
};
|
|
15
10
|
const TOOLTIPS = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
draws: "Draw Calls",
|
|
12
|
+
tris: "Triangles",
|
|
13
|
+
prims: "Primitives (lines + points)",
|
|
14
|
+
geoms: "Geometries",
|
|
15
|
+
textures: "Textures"
|
|
21
16
|
};
|
|
22
|
-
const COMPACT_UNITS = [
|
|
17
|
+
const COMPACT_UNITS = [
|
|
18
|
+
"K",
|
|
19
|
+
"M",
|
|
20
|
+
"B",
|
|
21
|
+
"T"
|
|
22
|
+
];
|
|
23
23
|
const NBSP = "\xA0";
|
|
24
24
|
function pad4(s) {
|
|
25
|
-
|
|
25
|
+
return s.length >= 4 ? s : s + NBSP.repeat(4 - s.length);
|
|
26
26
|
}
|
|
27
27
|
function formatCompact(n) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return pad4(String(rounded) + COMPACT_UNITS[unit]);
|
|
28
|
+
if (!Number.isFinite(n) || n < 0) return pad4("0");
|
|
29
|
+
if (n < 1e3) return pad4(String(Math.round(n)));
|
|
30
|
+
let unit = 0;
|
|
31
|
+
let val = n / 1e3;
|
|
32
|
+
while (val >= 1e3 && unit < COMPACT_UNITS.length - 1) {
|
|
33
|
+
val /= 1e3;
|
|
34
|
+
unit++;
|
|
35
|
+
}
|
|
36
|
+
if (val < 9.95) return pad4(val.toFixed(1) + COMPACT_UNITS[unit]);
|
|
37
|
+
const rounded = Math.round(val);
|
|
38
|
+
if (rounded >= 1e3 && unit < COMPACT_UNITS.length - 1) return pad4("1.0" + COMPACT_UNITS[unit + 1]);
|
|
39
|
+
return pad4(String(rounded) + COMPACT_UNITS[unit]);
|
|
42
40
|
}
|
|
43
41
|
const STYLE_MARKER = "data-tp-flatland-statsrow";
|
|
44
42
|
const CELL_CLASS = "tp-flatland-statsrow-cell";
|
|
45
43
|
function injectStyles() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
|
|
45
|
+
const style = document.createElement("style");
|
|
46
|
+
style.setAttribute(STYLE_MARKER, "");
|
|
47
|
+
style.textContent = `
|
|
50
48
|
.${CELL_CLASS} { position: relative; }
|
|
51
49
|
.${CELL_CLASS} + .${CELL_CLASS}::before {
|
|
52
50
|
content: '';
|
|
@@ -59,71 +57,71 @@ function injectStyles() {
|
|
|
59
57
|
pointer-events: none;
|
|
60
58
|
}
|
|
61
59
|
`;
|
|
62
|
-
|
|
60
|
+
document.head.appendChild(style);
|
|
63
61
|
}
|
|
64
62
|
function addStatsRow(parent, client) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
63
|
+
injectStyles();
|
|
64
|
+
const blade = parent.addBlade({ view: "separator" });
|
|
65
|
+
const bladeEl = blade.element;
|
|
66
|
+
bladeEl.innerHTML = "";
|
|
67
|
+
bladeEl.className = "tp-cntv";
|
|
68
|
+
bladeEl.style.cssText = "";
|
|
69
|
+
const row = document.createElement("div");
|
|
70
|
+
row.style.cssText = [
|
|
71
|
+
"display:grid",
|
|
72
|
+
"grid-template-columns:repeat(5,1fr)",
|
|
73
|
+
"align-items:center",
|
|
74
|
+
"height:calc(var(--cnt-usz, 20px) * 1.1)",
|
|
75
|
+
"padding:0 4px",
|
|
76
|
+
"font-size:12px",
|
|
77
|
+
"line-height:1",
|
|
78
|
+
"font-family:var(--tp-base-font-family, ui-monospace, monospace)",
|
|
79
|
+
"font-variant-numeric:tabular-nums",
|
|
80
|
+
"color:var(--tp-monitor-foreground-color)"
|
|
81
|
+
].join(";");
|
|
82
|
+
const keys = [
|
|
83
|
+
"draws",
|
|
84
|
+
"textures",
|
|
85
|
+
"geoms",
|
|
86
|
+
"prims",
|
|
87
|
+
"tris"
|
|
88
|
+
];
|
|
89
|
+
const valueEls = {};
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
const cell = document.createElement("div");
|
|
92
|
+
cell.className = CELL_CLASS;
|
|
93
|
+
cell.title = TOOLTIPS[key];
|
|
94
|
+
cell.style.cssText = "display:flex;align-items:center;gap:3px;padding-left:6px;overflow:hidden;white-space:nowrap;user-select:none;-webkit-user-select:none;-moz-user-select:none";
|
|
95
|
+
const iconWrap = document.createElement("span");
|
|
96
|
+
iconWrap.title = TOOLTIPS[key];
|
|
97
|
+
iconWrap.style.cssText = "display:inline-flex;flex-shrink:0;color:var(--tp-label-foreground-color);align-self:flex-end;transform:translateY(-1px)";
|
|
98
|
+
iconWrap.innerHTML = ICONS[key];
|
|
99
|
+
cell.appendChild(iconWrap);
|
|
100
|
+
const valueEl = document.createElement("span");
|
|
101
|
+
valueEl.title = TOOLTIPS[key];
|
|
102
|
+
valueEl.textContent = `0${NBSP}${NBSP}${NBSP}`;
|
|
103
|
+
valueEl.style.cssText = "min-width:4ch;text-align:left;flex-shrink:0";
|
|
104
|
+
cell.appendChild(valueEl);
|
|
105
|
+
row.appendChild(cell);
|
|
106
|
+
valueEls[key] = valueEl;
|
|
107
|
+
}
|
|
108
|
+
bladeEl.appendChild(row);
|
|
109
|
+
const unsubscribe = client.addListener((s) => {
|
|
110
|
+
valueEls.draws.textContent = formatCompact(s.drawCalls ?? 0);
|
|
111
|
+
valueEls.tris.textContent = formatCompact(s.triangles ?? 0);
|
|
112
|
+
valueEls.prims.textContent = formatCompact(s.primitives ?? 0);
|
|
113
|
+
valueEls.geoms.textContent = formatCompact(s.geometries ?? 0);
|
|
114
|
+
valueEls.textures.textContent = formatCompact(s.textures ?? 0);
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
element: bladeEl,
|
|
118
|
+
dispose() {
|
|
119
|
+
unsubscribe();
|
|
120
|
+
blade.dispose();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
125
123
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
//#endregion
|
|
125
|
+
export { addStatsRow };
|
|
126
|
+
|
|
129
127
|
//# sourceMappingURL=stats-row.js.map
|
package/dist/stats-row.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stats-row.ts"],"sourcesContent":["/**\n * Compact single-row renderer stats blade.\n *\n * Five equal-width cells separated by dim vertical dividers:\n * `✎ 3 | △ 10K | ◇ 42 | ⌂ 12 | ▦ 3`\n *\n * Each cell shows an outline icon on the left and a left-aligned,\n * compact-formatted number on the right. A native `title=` tooltip on each\n * cell exposes the full field name. Injected via the same \"replace a\n * separator blade's content\" trick that `stats-graph.ts` uses, so the blade\n * still gets a proper slot in the blade rack.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\nimport type { DevtoolsClient } from './devtools-client.js'\n\n/** Internal shape of the cells we render — matches the icon + tooltip tables below. */\ninterface StatsRowValues {\n draws: number\n tris: number\n /** Points + lines aggregated — not currently on the bus (reserved for future). */\n prims: number\n geoms: number\n textures: number\n}\n\nexport interface StatsRowHandle {\n readonly element: HTMLElement\n dispose(): void\n}\n\n// ── Icons ────────────────────────────────────────────────────────────────\n// Outline glyphs, stroked with currentColor so they inherit theme vars.\n// Sized to `calc(1em - 1px)` (→ 11px at the row's 12px font-size), which\n// lines up with the digit cap-height without looking too tall. Every path\n// fills a `(1,1)–(13,13)` box inside the 14×14 viewBox so all icons share\n// the same visual footprint.\n\nconst ICON_ATTRS =\n 'viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block;width:calc(1em - 1px);height:calc(1em - 1px)\"'\n\nconst ICONS: Record<keyof Required<StatsRowValues>, string> = {\n // Pencil — diagonal parallelogram body with a ferrule band near the eraser\n draws: `<svg ${ICON_ATTRS}><path d=\"M1 13 L3 13 L13 3 L11 1 L1 11 Z\"/><path d=\"M9 3 L11 5\"/></svg>`,\n // Isoceles triangle stretched to fill the 12×12 box\n tris: `<svg ${ICON_ATTRS}><path d=\"M7 1 L13 13 L1 13 Z\"/></svg>`,\n // Square (aggregated lines + points)\n prims: `<svg ${ICON_ATTRS}><path d=\"M1 1 L13 1 L13 13 L1 13 Z\"/></svg>`,\n // Regular-ish pentagon (dodecahedron silhouette), y-scaled to fill the box\n geoms: `<svg ${ICON_ATTRS}><path d=\"M7 1 L13 5.6 L10.7 13 L3.3 13 L1 5.6 Z\"/></svg>`,\n // 3×3 dot grid (filled — swap stroke→fill). Centers spread so outer dot\n // edges touch the same (1,1)–(13,13) box as the other icons.\n textures: `<svg viewBox=\"0 0 14 14\" fill=\"currentColor\" style=\"display:block;width:calc(1em - 1px);height:calc(1em - 1px)\"><circle cx=\"2.1\" cy=\"2.1\" r=\"1.1\"/><circle cx=\"7\" cy=\"2.1\" r=\"1.1\"/><circle cx=\"11.9\" cy=\"2.1\" r=\"1.1\"/><circle cx=\"2.1\" cy=\"7\" r=\"1.1\"/><circle cx=\"7\" cy=\"7\" r=\"1.1\"/><circle cx=\"11.9\" cy=\"7\" r=\"1.1\"/><circle cx=\"2.1\" cy=\"11.9\" r=\"1.1\"/><circle cx=\"7\" cy=\"11.9\" r=\"1.1\"/><circle cx=\"11.9\" cy=\"11.9\" r=\"1.1\"/></svg>`,\n}\n\nconst TOOLTIPS: Record<keyof Required<StatsRowValues>, string> = {\n draws: 'Draw Calls',\n tris: 'Triangles',\n prims: 'Primitives (lines + points)',\n geoms: 'Geometries',\n textures: 'Textures',\n}\n\n// ── Compact number formatting ────────────────────────────────────────────\n// Always produces exactly 4 characters (short values are right-padded with\n// non-breaking spaces so every cell renders a stable 4-char slot). Auto-\n// scales through K / M / B / T suffixes so triangle counts (the widest-\n// ranging stat — can hit hundreds of millions) stay legible without\n// widening the row.\n\nconst COMPACT_UNITS = ['K', 'M', 'B', 'T']\nconst NBSP = '\\u00A0'\n\nfunction pad4(s: string): string {\n return s.length >= 4 ? s : s + NBSP.repeat(4 - s.length)\n}\n\nfunction formatCompact(n: number): string {\n if (!Number.isFinite(n) || n < 0) return pad4('0')\n if (n < 1000) return pad4(String(Math.round(n)))\n\n let unit = 0\n let val = n / 1000\n while (val >= 1000 && unit < COMPACT_UNITS.length - 1) {\n val /= 1000\n unit++\n }\n\n // val ∈ [1, 1000). Under 10 shows one decimal (\"1.2K\" / \"9.9K\"); 10+\n // rounds to integer (\"10K\" / \"999K\"). If the integer would overflow\n // into the next magnitude (e.g. 999.5 → 1000), promote one unit as\n // \"1.0X\" so the output never grows beyond 4 characters.\n if (val < 9.95) return pad4(val.toFixed(1) + COMPACT_UNITS[unit])\n const rounded = Math.round(val)\n if (rounded >= 1000 && unit < COMPACT_UNITS.length - 1) {\n return pad4('1.0' + COMPACT_UNITS[unit + 1])\n }\n return pad4(String(rounded) + COMPACT_UNITS[unit])\n}\n\n// ── One-time <style> injection for divider pseudo-elements ───────────────\n// Pseudo-elements can't be set via inline style attributes, so we inject a\n// single stylesheet the first time `addStatsRow` runs. Uses a `data-*`\n// marker so repeat calls are idempotent.\n\nconst STYLE_MARKER = 'data-tp-flatland-statsrow'\nconst CELL_CLASS = 'tp-flatland-statsrow-cell'\n\nfunction injectStyles(): void {\n if (document.querySelector(`style[${STYLE_MARKER}]`)) return\n const style = document.createElement('style')\n style.setAttribute(STYLE_MARKER, '')\n style.textContent = `\n .${CELL_CLASS} { position: relative; }\n .${CELL_CLASS} + .${CELL_CLASS}::before {\n content: '';\n position: absolute;\n left: 0;\n top: 3px;\n bottom: 3px;\n width: 1px;\n background: var(--tp-groove-foreground-color, rgba(240, 237, 216, 0.12));\n pointer-events: none;\n }\n `\n document.head.appendChild(style)\n}\n\n// ── Blade construction ──────────────────────────────────────────────────\n\nexport function addStatsRow(parent: Pane | FolderApi, client: DevtoolsClient): StatsRowHandle {\n injectStyles()\n\n // Use a separator blade as a slot so ordering plays nicely with the rack,\n // then nuke its content and drop in our own layout.\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 bladeEl.style.cssText = ''\n\n const row = document.createElement('div')\n row.style.cssText = [\n // Five equal columns spanning the full blade width. Dividers between\n // cells are drawn by a `::before` pseudo-element on every cell after\n // the first (see injectStyles). Row padding is intentionally tight\n // (4px) because each column has only ~50px to fit a 4-char value plus\n // an icon — shaving 2px per side here gives the triangles cell enough\n // headroom for \"999K\" / \"1.2M\" without clipping.\n 'display:grid',\n 'grid-template-columns:repeat(5,1fr)',\n 'align-items:center',\n 'height:calc(var(--cnt-usz, 20px) * 1.1)',\n 'padding:0 4px',\n 'font-size:12px',\n 'line-height:1',\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\n const keys = ['draws', 'textures', 'geoms', 'prims', 'tris'] as const\n const valueEls = {} as Record<(typeof keys)[number], HTMLSpanElement>\n\n for (const key of keys) {\n const cell = document.createElement('div')\n cell.className = CELL_CLASS\n cell.title = TOOLTIPS[key]\n // Left-aligned icon + number. `padding-left: 6px` leaves breathing room\n // between the divider line and the icon; `gap: 3px` keeps the icon tight\n // against the number. Both are tuned to fit a 4-char value in the\n // narrowest cell (~49px) without clipping. `user-select: none` (plus\n // vendor prefixes for Safari/Firefox) so stats aren't accidentally\n // selectable or copyable — they're a readout, not content.\n cell.style.cssText =\n 'display:flex;align-items:center;gap:3px;padding-left:6px;overflow:hidden;white-space:nowrap;user-select:none;-webkit-user-select:none;-moz-user-select:none'\n\n const iconWrap = document.createElement('span')\n // Tooltip also attached here so hovering the icon (not just the number)\n // surfaces the field name. Bottom-aligned + shifted up 1px pins the\n // icon bottom to the digit baseline.\n iconWrap.title = TOOLTIPS[key]\n iconWrap.style.cssText =\n 'display:inline-flex;flex-shrink:0;color:var(--tp-label-foreground-color);align-self:flex-end;transform:translateY(-1px)'\n iconWrap.innerHTML = ICONS[key]\n cell.appendChild(iconWrap)\n\n // Value slot reserves 4 chars of width — `formatCompact` always emits\n // exactly 4 chars (padded with NBSP) so the icon stays planted. Title\n // mirrored here so the tooltip shows over the number too.\n const valueEl = document.createElement('span')\n valueEl.title = TOOLTIPS[key]\n valueEl.textContent = `0${NBSP}${NBSP}${NBSP}`\n valueEl.style.cssText = 'min-width:4ch;text-align:left;flex-shrink:0'\n cell.appendChild(valueEl)\n\n row.appendChild(cell)\n valueEls[key] = valueEl\n }\n\n bladeEl.appendChild(row)\n\n // Bus-driven: the client holds the full live state. On every update\n // we just render the current values — setting textContent to the\n // same string is a no-op, so diffing locally is unnecessary.\n const unsubscribe = client.addListener((s) => {\n valueEls.draws.textContent = formatCompact(s.drawCalls ?? 0)\n valueEls.tris.textContent = formatCompact(s.triangles ?? 0)\n valueEls.prims.textContent = formatCompact(s.primitives ?? 0)\n valueEls.geoms.textContent = formatCompact(s.geometries ?? 0)\n valueEls.textures.textContent = formatCompact(s.textures ?? 0)\n })\n\n return {\n element: bladeEl,\n dispose() {\n unsubscribe()\n blade.dispose()\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"stats-row.js","names":[],"sources":["../src/stats-row.ts"],"sourcesContent":["/**\n * Compact single-row renderer stats blade.\n *\n * Five equal-width cells separated by dim vertical dividers:\n * `✎ 3 | △ 10K | ◇ 42 | ⌂ 12 | ▦ 3`\n *\n * Each cell shows an outline icon on the left and a left-aligned,\n * compact-formatted number on the right. A native `title=` tooltip on each\n * cell exposes the full field name. Injected via the same \"replace a\n * separator blade's content\" trick that `stats-graph.ts` uses, so the blade\n * still gets a proper slot in the blade rack.\n */\n\nimport type { FolderApi, Pane } from 'tweakpane'\nimport type { DevtoolsClient } from './devtools-client.js'\n\n/** Internal shape of the cells we render — matches the icon + tooltip tables below. */\ninterface StatsRowValues {\n draws: number\n tris: number\n /** Points + lines aggregated — not currently on the bus (reserved for future). */\n prims: number\n geoms: number\n textures: number\n}\n\nexport interface StatsRowHandle {\n readonly element: HTMLElement\n dispose(): void\n}\n\n// ── Icons ────────────────────────────────────────────────────────────────\n// Outline glyphs, stroked with currentColor so they inherit theme vars.\n// Sized to `calc(1em - 1px)` (→ 11px at the row's 12px font-size), which\n// lines up with the digit cap-height without looking too tall. Every path\n// fills a `(1,1)–(13,13)` box inside the 14×14 viewBox so all icons share\n// the same visual footprint.\n\nconst ICON_ATTRS =\n 'viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block;width:calc(1em - 1px);height:calc(1em - 1px)\"'\n\nconst ICONS: Record<keyof Required<StatsRowValues>, string> = {\n // Pencil — diagonal parallelogram body with a ferrule band near the eraser\n draws: `<svg ${ICON_ATTRS}><path d=\"M1 13 L3 13 L13 3 L11 1 L1 11 Z\"/><path d=\"M9 3 L11 5\"/></svg>`,\n // Isoceles triangle stretched to fill the 12×12 box\n tris: `<svg ${ICON_ATTRS}><path d=\"M7 1 L13 13 L1 13 Z\"/></svg>`,\n // Square (aggregated lines + points)\n prims: `<svg ${ICON_ATTRS}><path d=\"M1 1 L13 1 L13 13 L1 13 Z\"/></svg>`,\n // Regular-ish pentagon (dodecahedron silhouette), y-scaled to fill the box\n geoms: `<svg ${ICON_ATTRS}><path d=\"M7 1 L13 5.6 L10.7 13 L3.3 13 L1 5.6 Z\"/></svg>`,\n // 3×3 dot grid (filled — swap stroke→fill). Centers spread so outer dot\n // edges touch the same (1,1)–(13,13) box as the other icons.\n textures: `<svg viewBox=\"0 0 14 14\" fill=\"currentColor\" style=\"display:block;width:calc(1em - 1px);height:calc(1em - 1px)\"><circle cx=\"2.1\" cy=\"2.1\" r=\"1.1\"/><circle cx=\"7\" cy=\"2.1\" r=\"1.1\"/><circle cx=\"11.9\" cy=\"2.1\" r=\"1.1\"/><circle cx=\"2.1\" cy=\"7\" r=\"1.1\"/><circle cx=\"7\" cy=\"7\" r=\"1.1\"/><circle cx=\"11.9\" cy=\"7\" r=\"1.1\"/><circle cx=\"2.1\" cy=\"11.9\" r=\"1.1\"/><circle cx=\"7\" cy=\"11.9\" r=\"1.1\"/><circle cx=\"11.9\" cy=\"11.9\" r=\"1.1\"/></svg>`,\n}\n\nconst TOOLTIPS: Record<keyof Required<StatsRowValues>, string> = {\n draws: 'Draw Calls',\n tris: 'Triangles',\n prims: 'Primitives (lines + points)',\n geoms: 'Geometries',\n textures: 'Textures',\n}\n\n// ── Compact number formatting ────────────────────────────────────────────\n// Always produces exactly 4 characters (short values are right-padded with\n// non-breaking spaces so every cell renders a stable 4-char slot). Auto-\n// scales through K / M / B / T suffixes so triangle counts (the widest-\n// ranging stat — can hit hundreds of millions) stay legible without\n// widening the row.\n\nconst COMPACT_UNITS = ['K', 'M', 'B', 'T']\nconst NBSP = '\\u00A0'\n\nfunction pad4(s: string): string {\n return s.length >= 4 ? s : s + NBSP.repeat(4 - s.length)\n}\n\nfunction formatCompact(n: number): string {\n if (!Number.isFinite(n) || n < 0) return pad4('0')\n if (n < 1000) return pad4(String(Math.round(n)))\n\n let unit = 0\n let val = n / 1000\n while (val >= 1000 && unit < COMPACT_UNITS.length - 1) {\n val /= 1000\n unit++\n }\n\n // val ∈ [1, 1000). Under 10 shows one decimal (\"1.2K\" / \"9.9K\"); 10+\n // rounds to integer (\"10K\" / \"999K\"). If the integer would overflow\n // into the next magnitude (e.g. 999.5 → 1000), promote one unit as\n // \"1.0X\" so the output never grows beyond 4 characters.\n if (val < 9.95) return pad4(val.toFixed(1) + COMPACT_UNITS[unit])\n const rounded = Math.round(val)\n if (rounded >= 1000 && unit < COMPACT_UNITS.length - 1) {\n return pad4('1.0' + COMPACT_UNITS[unit + 1])\n }\n return pad4(String(rounded) + COMPACT_UNITS[unit])\n}\n\n// ── One-time <style> injection for divider pseudo-elements ───────────────\n// Pseudo-elements can't be set via inline style attributes, so we inject a\n// single stylesheet the first time `addStatsRow` runs. Uses a `data-*`\n// marker so repeat calls are idempotent.\n\nconst STYLE_MARKER = 'data-tp-flatland-statsrow'\nconst CELL_CLASS = 'tp-flatland-statsrow-cell'\n\nfunction injectStyles(): void {\n if (document.querySelector(`style[${STYLE_MARKER}]`)) return\n const style = document.createElement('style')\n style.setAttribute(STYLE_MARKER, '')\n style.textContent = `\n .${CELL_CLASS} { position: relative; }\n .${CELL_CLASS} + .${CELL_CLASS}::before {\n content: '';\n position: absolute;\n left: 0;\n top: 3px;\n bottom: 3px;\n width: 1px;\n background: var(--tp-groove-foreground-color, rgba(240, 237, 216, 0.12));\n pointer-events: none;\n }\n `\n document.head.appendChild(style)\n}\n\n// ── Blade construction ──────────────────────────────────────────────────\n\nexport function addStatsRow(parent: Pane | FolderApi, client: DevtoolsClient): StatsRowHandle {\n injectStyles()\n\n // Use a separator blade as a slot so ordering plays nicely with the rack,\n // then nuke its content and drop in our own layout.\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 bladeEl.style.cssText = ''\n\n const row = document.createElement('div')\n row.style.cssText = [\n // Five equal columns spanning the full blade width. Dividers between\n // cells are drawn by a `::before` pseudo-element on every cell after\n // the first (see injectStyles). Row padding is intentionally tight\n // (4px) because each column has only ~50px to fit a 4-char value plus\n // an icon — shaving 2px per side here gives the triangles cell enough\n // headroom for \"999K\" / \"1.2M\" without clipping.\n 'display:grid',\n 'grid-template-columns:repeat(5,1fr)',\n 'align-items:center',\n 'height:calc(var(--cnt-usz, 20px) * 1.1)',\n 'padding:0 4px',\n 'font-size:12px',\n 'line-height:1',\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\n const keys = ['draws', 'textures', 'geoms', 'prims', 'tris'] as const\n const valueEls = {} as Record<(typeof keys)[number], HTMLSpanElement>\n\n for (const key of keys) {\n const cell = document.createElement('div')\n cell.className = CELL_CLASS\n cell.title = TOOLTIPS[key]\n // Left-aligned icon + number. `padding-left: 6px` leaves breathing room\n // between the divider line and the icon; `gap: 3px` keeps the icon tight\n // against the number. Both are tuned to fit a 4-char value in the\n // narrowest cell (~49px) without clipping. `user-select: none` (plus\n // vendor prefixes for Safari/Firefox) so stats aren't accidentally\n // selectable or copyable — they're a readout, not content.\n cell.style.cssText =\n 'display:flex;align-items:center;gap:3px;padding-left:6px;overflow:hidden;white-space:nowrap;user-select:none;-webkit-user-select:none;-moz-user-select:none'\n\n const iconWrap = document.createElement('span')\n // Tooltip also attached here so hovering the icon (not just the number)\n // surfaces the field name. Bottom-aligned + shifted up 1px pins the\n // icon bottom to the digit baseline.\n iconWrap.title = TOOLTIPS[key]\n iconWrap.style.cssText =\n 'display:inline-flex;flex-shrink:0;color:var(--tp-label-foreground-color);align-self:flex-end;transform:translateY(-1px)'\n iconWrap.innerHTML = ICONS[key]\n cell.appendChild(iconWrap)\n\n // Value slot reserves 4 chars of width — `formatCompact` always emits\n // exactly 4 chars (padded with NBSP) so the icon stays planted. Title\n // mirrored here so the tooltip shows over the number too.\n const valueEl = document.createElement('span')\n valueEl.title = TOOLTIPS[key]\n valueEl.textContent = `0${NBSP}${NBSP}${NBSP}`\n valueEl.style.cssText = 'min-width:4ch;text-align:left;flex-shrink:0'\n cell.appendChild(valueEl)\n\n row.appendChild(cell)\n valueEls[key] = valueEl\n }\n\n bladeEl.appendChild(row)\n\n // Bus-driven: the client holds the full live state. On every update\n // we just render the current values — setting textContent to the\n // same string is a no-op, so diffing locally is unnecessary.\n const unsubscribe = client.addListener((s) => {\n valueEls.draws.textContent = formatCompact(s.drawCalls ?? 0)\n valueEls.tris.textContent = formatCompact(s.triangles ?? 0)\n valueEls.prims.textContent = formatCompact(s.primitives ?? 0)\n valueEls.geoms.textContent = formatCompact(s.geometries ?? 0)\n valueEls.textures.textContent = formatCompact(s.textures ?? 0)\n })\n\n return {\n element: bladeEl,\n dispose() {\n unsubscribe()\n blade.dispose()\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region src/theme.d.ts
|
|
1
2
|
/**
|
|
2
3
|
* Flatland theme — based on the Jehkoba32 retro palette.
|
|
3
4
|
*
|
|
@@ -6,5 +7,6 @@
|
|
|
6
7
|
*/
|
|
7
8
|
declare const FLATLAND_THEME: Record<string, string>;
|
|
8
9
|
declare function applyTheme(element: HTMLElement, theme?: Record<string, string>): void;
|
|
9
|
-
|
|
10
|
+
//#endregion
|
|
10
11
|
export { FLATLAND_THEME, applyTheme };
|
|
12
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","names":[],"sources":["../src/theme.ts"],"mappings":";;;;;;;cAMa,gBAAgB;iBAuNb,WAAW,SAAS,aAAa,QAAO"}
|