@zkmake/three-meter 0.9.1 → 0.10.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-monitor-BqnoiLEs.js","names":[],"sources":["../src/core/environment.ts","../src/core/frame-stats.ts","../src/core/gpu-timer.ts","../src/core/ring-buffer.ts","../src/core/scene-cost.ts","../src/core/performance-monitor.ts"],"sourcesContent":["/**\n * What the monitor is running on: three revision, rendering backend, GPU.\n * Read from the renderer and the page, never from an import of three.\n *\n * `WebGPURenderer` settles its backend in `init()`, so until then the backend\n * reads `null`; callers poll and the monitor caches once it is known.\n */\nimport type { Environment, PerfRenderer, RenderBackend } from \"./types.ts\";\n\ntype BackendShape = {\n device?: { adapterInfo?: AdapterInfoShape } | null;\n gl?: unknown;\n isWebGLBackend?: boolean;\n isWebGPUBackend?: boolean;\n parameters?: { forceWebGL?: boolean };\n};\n\ntype AdapterInfoShape = {\n architecture?: string;\n description?: string;\n device?: string;\n vendor?: string;\n};\n\ntype DebugRendererInfoExt = { readonly UNMASKED_RENDERER_WEBGL: number };\n\n/** three sets `window.__THREE__` to its revision on import (and warns on a second copy). */\nconst readThreeRevision = (): string | null => {\n const revision = (globalThis as { __THREE__?: unknown }).__THREE__;\n\n return typeof revision === \"string\" && revision !== \"\" ? revision : null;\n};\n\n/**\n * Trim a WebGL renderer string to the part a person recognises. Chrome wraps\n * it in ANGLE, e.g. `ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786)\n * Direct3D11 vs_5_0 ps_5_0, D3D11)` or `ANGLE (Apple, ANGLE Metal Renderer:\n * Apple M3 Pro, Unspecified Version)`.\n */\nconst cleanGpuName = (raw: string): string => {\n let name = raw.trim();\n const angle = /^ANGLE \\((.*)\\)$/.exec(name);\n\n if (angle) {\n const parts = angle[1]!.split(\", \");\n name = parts[1] ?? parts[0]!;\n }\n\n return name\n .replace(/^ANGLE [\\w ]+ Renderer: /, \"\")\n .replace(/ \\(0x[0-9a-f]+\\)/i, \"\")\n .replace(/ (Direct3D\\d*|OpenGL|Vulkan|Metal)\\b.*$/, \"\")\n .trim();\n};\n\nconst webglGpuName = (gl: WebGLRenderingContext | WebGL2RenderingContext): string | null => {\n // Firefox deprecates the extension but already returns the unmasked name from RENDERER.\n const ext = gl.getExtension(\"WEBGL_debug_renderer_info\") as DebugRendererInfoExt | null;\n const raw = gl.getParameter(ext ? ext.UNMASKED_RENDERER_WEBGL : gl.RENDERER) as unknown;\n\n return typeof raw === \"string\" && raw !== \"\" && raw !== \"WebKit WebGL\" ? cleanGpuName(raw) : null;\n};\n\n/** WebGPU reports vendors lowercase (`apple`, `nvidia`). */\nconst VENDOR_NAMES: Record<string, string> = {\n amd: \"AMD\",\n apple: \"Apple\",\n arm: \"Arm\",\n intel: \"Intel\",\n nvidia: \"NVIDIA\",\n qualcomm: \"Qualcomm\",\n};\n\n/**\n * `GPUDevice.adapterInfo`. Chrome leaves `description` empty to limit\n * fingerprinting, so this is usually vendor + architecture: `Apple metal-3`.\n */\nconst webgpuGpuName = (info: AdapterInfoShape | undefined): string | null => {\n if (!info) {\n return null;\n }\n\n if (info.description) {\n return info.description;\n }\n\n const vendor = info.vendor ? (VENDOR_NAMES[info.vendor] ?? info.vendor) : \"\";\n const name = [vendor, info.architecture].filter(Boolean).join(\" \");\n\n return name === \"\" ? null : name;\n};\n\nconst isWebglContext = (value: unknown): value is WebGLRenderingContext | WebGL2RenderingContext =>\n (typeof WebGL2RenderingContext !== \"undefined\" && value instanceof WebGL2RenderingContext) ||\n (typeof WebGLRenderingContext !== \"undefined\" && value instanceof WebGLRenderingContext);\n\nconst webglBackend = (gl: WebGLRenderingContext | WebGL2RenderingContext): RenderBackend =>\n typeof WebGL2RenderingContext !== \"undefined\" && gl instanceof WebGL2RenderingContext\n ? \"webgl2\"\n : \"webgl\";\n\nconst readEnvironment = (renderer: PerfRenderer): Environment => {\n const three = readThreeRevision();\n const backend = renderer.backend as BackendShape | undefined;\n\n // WebGPURenderer. Read the backend's own fields: `getContext()` before\n // `init()` is what the GPU timer learned not to touch.\n if (backend) {\n if (backend.isWebGPUBackend) {\n const device = backend.device;\n\n return device\n ? { backend: \"webgpu\", fallback: false, gpu: webgpuGpuName(device.adapterInfo), three }\n : { backend: null, fallback: false, gpu: null, three };\n }\n\n if (backend.isWebGLBackend && isWebglContext(backend.gl)) {\n return {\n backend: webglBackend(backend.gl),\n fallback: backend.parameters?.forceWebGL !== true,\n gpu: webglGpuName(backend.gl),\n three,\n };\n }\n\n return { backend: null, fallback: false, gpu: null, three };\n }\n\n // WebGLRenderer.\n const context = renderer.getContext?.();\n\n return isWebglContext(context)\n ? { backend: webglBackend(context), fallback: false, gpu: webglGpuName(context), three }\n : { backend: null, fallback: false, gpu: null, three };\n};\n\nexport { cleanGpuName, readEnvironment };\n","/**\n * Stutter statistics over a window of frame intervals. Average FPS hides a\n * hitch every few seconds; these don't. Stalls (hidden tab, breakpoint) never\n * reach the window, so they don't count as hitches.\n */\nimport type { FrameStats } from \"./types.ts\";\n\n/** A frame over this multiple of the window's median interval is a hitch. */\nconst HITCH_FACTOR = 2;\n\n/** Fewer frames than this and the refresh rate reads `null`. */\nconst REFRESH_MIN_FRAMES = 60;\n\n/** Common display refresh rates. A reading within 4% of one snaps to it. */\nconst REFRESH_RATES = [30, 48, 50, 60, 72, 75, 90, 100, 120, 144, 165, 180, 240, 360];\n\nconst EMPTY: FrameStats = Object.freeze({\n frames: 0,\n hitches: 0,\n lowFps: 0,\n p99Ms: 0,\n refreshHz: null,\n});\n\n/**\n * The rate the loop runs at when it keeps up: the fastest tenth of frames,\n * not the average, so slow frames don't drag it down. That is the display's\n * refresh rate, or the app's own cap if it throttles itself.\n *\n * Intervals are averaged with their neighbour first. A frame that starts late\n * leaves a short interval after it, and on a real 120 Hz display those short\n * ones alone read as 125–127 Hz; each pair averages back to the vsync.\n */\nconst refreshFrom = (intervals: readonly number[]): number | null => {\n if (intervals.length < REFRESH_MIN_FRAMES) {\n return null;\n }\n\n const pairs = intervals.slice(1).map((interval, index) => (interval + intervals[index]!) / 2);\n pairs.sort((a, b) => a - b);\n const rate = 1000 / pairs[Math.floor(pairs.length * 0.1)]!;\n const nearest = REFRESH_RATES.reduce((best, candidate) =>\n Math.abs(candidate - rate) < Math.abs(best - rate) ? candidate : best,\n );\n\n return Math.abs(nearest - rate) / nearest <= 0.04 ? nearest : Math.round(rate);\n};\n\nconst computeFrameStats = (intervals: readonly number[]): FrameStats => {\n const frames = intervals.length;\n\n if (frames === 0) {\n return EMPTY;\n }\n\n const sorted = [...intervals].sort((a, b) => a - b);\n // Nearest rank: the smallest interval at or above 99% of frames.\n const p99Ms = sorted[Math.ceil(frames * 0.99) - 1]!;\n\n // 1% low: the mean rate across the slowest 1% of frames (at least one frame).\n const slowest = Math.max(1, Math.ceil(frames * 0.01));\n let slowSum = 0;\n\n for (let index = frames - slowest; index < frames; index += 1) {\n slowSum += sorted[index]!;\n }\n\n const middle = frames >> 1;\n const median = frames % 2 === 0 ? (sorted[middle - 1]! + sorted[middle]!) / 2 : sorted[middle]!;\n const threshold = median * HITCH_FACTOR;\n let hitches = 0;\n\n for (const interval of intervals) {\n if (interval > threshold) {\n hitches += 1;\n }\n }\n\n return Object.freeze({\n frames,\n hitches,\n lowFps: 1000 / (slowSum / slowest),\n p99Ms,\n refreshHz: refreshFrom(intervals),\n });\n};\n\nexport { computeFrameStats, HITCH_FACTOR };\n","/**\n * Real GPU frame timing.\n *\n * WebGPU: Three's `trackTimestamp` + `resolveTimestampsAsync` (ms, a frame or\n * two late). Never calls `getExtension`: that is what remounted the canvas.\n *\n * WebGL2 fallback (Safari without WebGPU, or a forced GL backend):\n * `EXT_disjoint_timer_query_webgl2`, same pool-of-queries pattern as the\n * platform-typing monitor.\n */\nimport type { PerfRenderer } from \"./types.ts\";\n\ntype DisjointTimerQueryExt = {\n readonly GPU_DISJOINT_EXT: number;\n readonly TIME_ELAPSED_EXT: number;\n};\n\nclass GpuTimer {\n private lastMs = 0;\n private readonly webgpu: PerfRenderer | null;\n private webgpuInFlight = false;\n private readonly gl: WebGL2RenderingContext | null;\n private readonly ext: DisjointTimerQueryExt | null;\n private readonly free: WebGLQuery[] = [];\n private readonly pending: WebGLQuery[] = [];\n private active: WebGLQuery | null = null;\n\n private constructor(\n webgpu: PerfRenderer | null,\n gl: WebGL2RenderingContext | null,\n ext: DisjointTimerQueryExt | null,\n poolSize: number,\n ) {\n this.webgpu = webgpu;\n this.gl = gl;\n this.ext = ext;\n\n if (!gl) {\n return;\n }\n\n for (let index = 0; index < poolSize; index += 1) {\n const query = gl.createQuery();\n\n if (query) {\n this.free.push(query);\n }\n }\n }\n\n static create(renderer: PerfRenderer, poolSize: number): GpuTimer | null {\n const backend = renderer.backend as { trackTimestamp?: boolean } | undefined;\n\n if (backend?.trackTimestamp && renderer.resolveTimestampsAsync) {\n return new GpuTimer(renderer, null, null, poolSize);\n }\n\n const context = renderer.getContext?.();\n\n if (!(context instanceof WebGL2RenderingContext)) {\n return null;\n }\n\n const ext = context.getExtension(\n \"EXT_disjoint_timer_query_webgl2\",\n ) as DisjointTimerQueryExt | null;\n\n return ext ? new GpuTimer(null, context, ext, poolSize) : null;\n }\n\n begin() {\n if (this.webgpu || !this.gl || !this.ext) {\n return;\n }\n\n const query = this.free.pop();\n\n if (!query) {\n this.active = null;\n\n return;\n }\n\n this.active = query;\n this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query);\n }\n\n end() {\n if (this.webgpu || !this.gl || !this.ext || !this.active) {\n return;\n }\n\n this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);\n this.pending.push(this.active);\n this.active = null;\n }\n\n poll() {\n if (this.webgpu) {\n this.pollWebgpu();\n\n return;\n }\n\n this.pollWebgl();\n }\n\n getMs() {\n return this.lastMs;\n }\n\n dispose() {\n if (this.gl && this.ext && this.active) {\n this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);\n this.active = null;\n }\n\n if (this.gl) {\n for (const query of this.free) {\n this.gl.deleteQuery(query);\n }\n\n for (const query of this.pending) {\n this.gl.deleteQuery(query);\n }\n }\n\n this.free.length = 0;\n this.pending.length = 0;\n }\n\n private pollWebgpu() {\n const resolve = this.webgpu?.resolveTimestampsAsync;\n\n if (!resolve || this.webgpuInFlight) {\n return;\n }\n\n this.webgpuInFlight = true;\n\n void resolve\n .call(this.webgpu, \"render\")\n .then((ms) => {\n if (typeof ms === \"number\" && Number.isFinite(ms)) {\n this.lastMs = ms;\n }\n\n this.webgpuInFlight = false;\n })\n .catch(() => {\n this.webgpuInFlight = false;\n });\n }\n\n private pollWebgl() {\n if (!this.gl || !this.ext) {\n return;\n }\n\n const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT) as boolean;\n\n if (disjoint) {\n while (this.pending.length > 0) {\n this.free.push(this.pending.shift()!);\n }\n\n return;\n }\n\n while (this.pending.length > 0) {\n const query = this.pending[0]!;\n const ready = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE) as boolean;\n\n if (!ready) {\n break;\n }\n\n this.pending.shift();\n const ns = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT) as number;\n this.lastMs = ns / 1e6;\n this.free.push(query);\n }\n }\n}\n\nexport { GpuTimer };\n","/**\n * Fixed-capacity numeric ring buffer. Backs the per-frame timing history the\n * view reads to draw sparklines. Push is O(1); `toArray()` returns values in\n * chronological (oldest → newest) order.\n */\nclass RingBuffer {\n private readonly data: Float32Array;\n private readonly capacity: number;\n private head = 0;\n private count = 0;\n\n constructor(capacity: number) {\n this.capacity = capacity;\n this.data = new Float32Array(capacity);\n }\n\n push(value: number) {\n this.data[this.head] = value;\n this.head = (this.head + 1) % this.capacity;\n\n if (this.count < this.capacity) {\n this.count += 1;\n }\n }\n\n /** Values oldest → newest. Length grows up to capacity as samples accumulate. */\n toArray(): number[] {\n const out = Array.from<number>({ length: this.count });\n const start = (this.head - this.count + this.capacity) % this.capacity;\n\n for (let index = 0; index < this.count; index += 1) {\n out[index] = this.data[(start + index) % this.capacity]!;\n }\n\n return out;\n }\n\n clear() {\n this.head = 0;\n this.count = 0;\n }\n}\n\nexport { RingBuffer };\n","/**\n * Where a frame's draw calls and triangles come from, estimated from the\n * scene graph the way three's renderer walks it: hidden objects, other\n * layers and anything outside the camera's frustum are left out; each\n * visible material (or material group) is one draw call, two for a\n * transparent double-sided material. Main pass only: shadow maps and\n * post-processing passes aren't counted.\n *\n * Structural like the rest of core: no import of three, every field read\n * defensively, since this runs against whatever the app renders.\n */\nimport type { CostEntry, SceneCost } from \"./types.ts\";\n\ntype Elements = ArrayLike<number>;\n\ntype Sphere = { center: { x: number; y: number; z: number }; radius: number };\n\ntype GeometryShape = {\n attributes?: { position?: { count: number } };\n boundingSphere?: Sphere | null;\n drawRange?: { count: number; start: number };\n groups?: { count: number; materialIndex?: number; start: number }[];\n index?: { count: number } | null;\n name?: string;\n uuid?: string;\n};\n\ntype MaterialShape = {\n uuid?: string;\n forceSinglePass?: boolean;\n name?: string;\n side?: number;\n transparent?: boolean;\n type?: string;\n visible?: boolean;\n};\n\ntype ObjectShape = {\n boundingSphere?: Sphere | null;\n count?: number;\n frustumCulled?: boolean;\n geometry?: GeometryShape;\n isInstancedMesh?: boolean;\n isLine?: boolean;\n isMesh?: boolean;\n isPoints?: boolean;\n isSprite?: boolean;\n layers?: { mask: number };\n material?: MaterialShape | MaterialShape[];\n matrixWorld?: { elements: Elements };\n name?: string;\n type?: string;\n};\n\ntype CameraShape = {\n coordinateSystem?: number;\n layers?: { mask: number };\n matrixWorldInverse?: { elements: Elements };\n projectionMatrix?: { elements: Elements };\n reversedDepth?: boolean;\n};\n\ntype SceneShape = { traverseVisible?: (callback: (object: ObjectShape) => void) => void };\n\n/** three's `DoubleSide` and `WebGPUCoordinateSystem`. */\nconst DOUBLE_SIDE = 2;\nconst WEBGPU_COORDINATES = 2001;\n\ntype Plane = [number, number, number, number];\n\n/** Column-major `a × b`, as three stores matrices. */\nconst multiply = (a: Elements, b: Elements) => {\n const out = Array.from<number>({ length: 16 });\n\n for (let column = 0; column < 4; column += 1) {\n for (let row = 0; row < 4; row += 1) {\n let sum = 0;\n\n for (let k = 0; k < 4; k += 1) {\n sum += a[k * 4 + row]! * b[column * 4 + k]!;\n }\n\n out[column * 4 + row] = sum;\n }\n }\n\n return out;\n};\n\nconst normalize = (plane: Plane): Plane => {\n const length = Math.hypot(plane[0], plane[1], plane[2]) || 1;\n\n return [plane[0] / length, plane[1] / length, plane[2] / length, plane[3] / length];\n};\n\n/** Mirrors three's `Frustum.setFromProjectionMatrix`. Near and far only where the depth range is known. */\nconst frustumPlanes = (camera: CameraShape): Plane[] | null => {\n if (!camera.projectionMatrix || !camera.matrixWorldInverse) {\n return null;\n }\n\n const m = multiply(camera.projectionMatrix.elements, camera.matrixWorldInverse.elements);\n const row = (index: number): Plane => [m[index]!, m[index + 4]!, m[index + 8]!, m[index + 12]!];\n const [r0, r1, r2, r3] = [row(0), row(1), row(2), row(3)];\n const add = (a: Plane, b: Plane): Plane => [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];\n const sub = (a: Plane, b: Plane): Plane => [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]];\n\n const planes = [sub(r3, r0), add(r3, r0), add(r3, r1), sub(r3, r1)];\n\n if (!camera.reversedDepth) {\n planes.push(sub(r3, r2), camera.coordinateSystem === WEBGPU_COORDINATES ? r2 : add(r3, r2));\n }\n\n return planes.map(normalize);\n};\n\nconst inFrustum = (object: ObjectShape, planes: Plane[] | null) => {\n const sphere = object.boundingSphere ?? object.geometry?.boundingSphere;\n const e = object.matrixWorld?.elements;\n\n // three computes bounding spheres lazily while rendering; without one, count the object.\n if (!planes || !sphere || !e) {\n return true;\n }\n\n const { x, y, z } = sphere.center;\n const cx = e[0]! * x + e[4]! * y + e[8]! * z + e[12]!;\n const cy = e[1]! * x + e[5]! * y + e[9]! * z + e[13]!;\n const cz = e[2]! * x + e[6]! * y + e[10]! * z + e[14]!;\n const scale = Math.sqrt(\n Math.max(\n e[0]! ** 2 + e[1]! ** 2 + e[2]! ** 2,\n e[4]! ** 2 + e[5]! ** 2 + e[6]! ** 2,\n e[8]! ** 2 + e[9]! ** 2 + e[10]! ** 2,\n ),\n );\n const radius = sphere.radius * scale;\n\n // three's own test, `distance < -radius`. Written this way round a NaN plane (a camera with a\n // zero-size aspect, say) keeps the object, as three does, rather than hiding everything.\n return !planes.some(([a, b, c, d]) => a * cx + b * cy + c * cz + d < -radius);\n};\n\nconst layersMatch = (object: ObjectShape, camera: CameraShape) =>\n !object.layers || !camera.layers || (object.layers.mask & camera.layers.mask) !== 0;\n\nconst passes = (material: MaterialShape) =>\n material.transparent === true &&\n material.side === DOUBLE_SIDE &&\n material.forceSinglePass !== true\n ? 2\n : 1;\n\n/** Vertices a draw covers, clipped to `drawRange` and, when given, a group. */\nconst drawnVertices = (\n geometry: GeometryShape,\n group?: { count: number; start: number },\n): number => {\n const total = geometry.index?.count ?? geometry.attributes?.position?.count ?? 0;\n const rangeStart = geometry.drawRange?.start ?? 0;\n const rangeEnd = Math.min(total, rangeStart + (geometry.drawRange?.count ?? Infinity));\n const start = Math.max(rangeStart, group?.start ?? 0);\n const end = Math.min(rangeEnd, group ? group.start + group.count : Infinity);\n\n return Math.max(0, end - start);\n};\n\ntype Draw = { calls: number; material: MaterialShape; triangles: number };\n\n/** One draw per visible material, or per group when the material is an array. */\nconst drawsOf = (object: ObjectShape): Draw[] => {\n const { geometry, material } = object;\n\n if (!geometry || !material) {\n return [];\n }\n\n const instances = object.isInstancedMesh ? (object.count ?? 1) : 1;\n const trianglesFor = (vertices: number) =>\n object.isMesh ? Math.floor(vertices / 3) * instances : 0;\n\n if (Array.isArray(material)) {\n const draws: Draw[] = [];\n\n for (const group of geometry.groups ?? []) {\n const groupMaterial = material[group.materialIndex ?? 0];\n\n if (groupMaterial && groupMaterial.visible !== false) {\n const count = passes(groupMaterial);\n draws.push({\n calls: count,\n material: groupMaterial,\n triangles: trianglesFor(drawnVertices(geometry, group)) * count,\n });\n }\n }\n\n return draws;\n }\n\n return material.visible === false\n ? []\n : [\n {\n calls: passes(material),\n material,\n // A second pass draws the triangles again, and three's info counts them again.\n triangles: trianglesFor(drawnVertices(geometry)) * passes(material),\n },\n ];\n};\n\nconst objectLabel = (object: ObjectShape) =>\n object.name || object.geometry?.name || object.type || \"Object\";\n\nconst materialLabel = (material: MaterialShape) => material.name || material.type || \"Material\";\n\n/** A stable id for objects without a `uuid`, so keys survive between walks. */\nconst ids = new WeakMap<object, string>();\nlet nextId = 0;\n\nconst idOf = (value: object & { uuid?: string }) => {\n if (value.uuid) {\n return value.uuid;\n }\n\n let id = ids.get(value);\n\n if (!id) {\n nextId += 1;\n id = `#${nextId}`;\n ids.set(value, id);\n }\n\n return id;\n};\n\nconst entryFor = (groups: Map<unknown, CostEntry>, group: unknown, key: string, label: string) => {\n let entry = groups.get(group);\n\n if (!entry) {\n entry = { calls: 0, instances: 0, key, label, objects: [], triangles: 0 };\n groups.set(group, entry);\n }\n\n return entry;\n};\n\nconst byCost = (a: CostEntry, b: CostEntry) => b.calls - a.calls || b.triangles - a.triangles;\n\n/** Same label and geometry is one mesh row. */\nconst meshKeyOf = (object: ObjectShape) =>\n `mesh:${objectLabel(object)}\\u0000${object.geometry?.uuid ?? \"\"}`;\n\nconst materialsOf = (object: ObjectShape) =>\n object.material === undefined\n ? []\n : Array.isArray(object.material)\n ? object.material\n : [object.material];\n\n/**\n * Every drawable object in the scene that belongs to a row's `key`, on screen\n * or not: hiding a row has to catch the copies that orbit into view later.\n * Skips objects that are already hidden, like three does.\n */\nconst objectsForKey = (scene: unknown, key: string): unknown[] => {\n const root = scene as SceneShape;\n const found: unknown[] = [];\n\n if (typeof root?.traverseVisible !== \"function\") {\n return found;\n }\n\n root.traverseVisible((object) => {\n const drawable = object.isMesh || object.isLine || object.isPoints || object.isSprite;\n\n if (!drawable) {\n return;\n }\n\n const matches = key.startsWith(\"mesh:\")\n ? meshKeyOf(object) === key\n : materialsOf(object).some((material) => `material:${idOf(material)}` === key);\n\n if (matches) {\n found.push(object);\n }\n });\n\n return found;\n};\n\nconst computeSceneCost = (scene: unknown, camera: unknown): SceneCost | null => {\n const root = scene as SceneShape;\n const view = camera as CameraShape;\n\n if (typeof root?.traverseVisible !== \"function\") {\n return null;\n }\n\n const planes = frustumPlanes(view);\n // Same label and geometry is one row, so 500 unmerged copies read as \"tree ×500\".\n const meshes = new Map<unknown, CostEntry>();\n const materials = new Map<unknown, CostEntry>();\n let calls = 0;\n let triangles = 0;\n\n root.traverseVisible((object) => {\n const drawable = object.isMesh || object.isLine || object.isPoints || object.isSprite;\n\n if (!drawable || !layersMatch(object, view)) {\n return;\n }\n\n if (object.frustumCulled !== false && !object.isSprite && !inFrustum(object, planes)) {\n return;\n }\n\n const instances = object.isInstancedMesh ? (object.count ?? 1) : 1;\n const label = objectLabel(object);\n const meshKey = meshKeyOf(object);\n\n const draws = drawsOf(object);\n\n if (draws.length === 0) {\n return;\n }\n\n const mesh = entryFor(meshes, meshKey, meshKey, label);\n mesh.objects.push(object);\n mesh.instances += instances;\n // A multi-material object can draw one material in several groups; list it once per material.\n const listed = new Set<MaterialShape>();\n\n for (const draw of draws) {\n calls += draw.calls;\n triangles += draw.triangles;\n mesh.calls += draw.calls;\n mesh.triangles += draw.triangles;\n\n const material = entryFor(\n materials,\n draw.material,\n `material:${idOf(draw.material)}`,\n materialLabel(draw.material),\n );\n material.calls += draw.calls;\n material.triangles += draw.triangles;\n\n if (!listed.has(draw.material)) {\n listed.add(draw.material);\n material.objects.push(object);\n material.instances += instances;\n }\n }\n });\n\n return {\n calls,\n materials: [...materials.values()].sort(byCost),\n meshes: [...meshes.values()].sort(byCost),\n triangles,\n };\n};\n\nexport { computeSceneCost, objectsForKey };\n","import { readEnvironment } from \"./environment.ts\";\nimport { computeFrameStats } from \"./frame-stats.ts\";\nimport { GpuTimer } from \"./gpu-timer.ts\";\nimport { RingBuffer } from \"./ring-buffer.ts\";\nimport { computeSceneCost, objectsForKey } from \"./scene-cost.ts\";\nimport type {\n CostEntry,\n Environment,\n FrameStats,\n PerfRenderer,\n PerformanceMonitorOptions,\n Sample,\n SceneCost,\n TimingMetric,\n} from \"./types.ts\";\n\nconst DEFAULT_GPU_POOL_SIZE = 5;\n/** One monitor per renderer: a second would stack the `render` patch and double-count passes. */\nconst ATTACHED = new WeakSet<PerfRenderer>();\nconst DEFAULT_HISTORY_SIZE = 120;\nconst DEFAULT_FRAME_STATS_SIZE = 1000;\nconst FPS_SMOOTHING = 0.1;\n/**\n * A frame interval past this is a stall (hidden tab, breakpoint, laptop lid),\n * not a slow frame. It is left out of FPS so one gap doesn't flatten the\n * graph's scale for the whole history window.\n */\nconst STALL_MS = 1000;\n\nconst frameCalls = (info: PerfRenderer[\"info\"]) => info.render.drawCalls ?? info.render.calls;\n\nconst programCount = (info: PerfRenderer[\"info\"]) =>\n info.memory.programs ?? info.programs?.length ?? 0;\n\n/**\n * Collector of three.js frame metrics. Bracket each frame with {@link begin}/\n * {@link end}, or let {@link wrapAnimationLoop} or the R3F `PerfSampler` do it.\n * Works on WebGPURenderer (drawCalls, memory.programs, timestamp queries) and\n * on a WebGL renderer (calls, programs[], timer-query ext).\n *\n * While attached, `renderer.info.autoReset` is off and `info` is reset in\n * {@link begin}; host code that reads `renderer.info` mid-frame sees this\n * frame's counts only. {@link dispose} restores both.\n *\n * CPU time runs from {@link begin} to the return of the frame's last\n * `render()` call, which the `render` patch stamps. So it holds even when\n * {@link end} is only reached on the next tick, as in the R3F sampler: it is\n * the JS work of the frame, not the interval to the next one.\n */\nclass PerformanceMonitor {\n private readonly renderer: PerfRenderer;\n private readonly gpuTimer: GpuTimer | null;\n private readonly originalRender: PerfRenderer[\"render\"];\n private renderPasses = 0;\n private lastRenderEndAt: number | null = null;\n private readonly fpsHistory: RingBuffer;\n private readonly cpuHistory: RingBuffer;\n private readonly gpuHistory: RingBuffer;\n private readonly frameIntervals: RingBuffer;\n private frameStats: FrameStats | null = null;\n /** The render call with the most draw calls this frame, and the scene and camera it drew. */\n private mainPassCalls = -1;\n private mainPass: { camera: WeakRef<object>; scene: WeakRef<object> } | null = null;\n private cpuStart = 0;\n private lastBeginAt: number | null = null;\n private frameFps = 0;\n private smoothedFps = 0;\n private sample: Sample;\n private environment: Environment | null = null;\n\n constructor(options: PerformanceMonitorOptions) {\n const {\n renderer,\n trackGPU = true,\n gpuQueryPoolSize = DEFAULT_GPU_POOL_SIZE,\n historySize = DEFAULT_HISTORY_SIZE,\n frameStatsSize = DEFAULT_FRAME_STATS_SIZE,\n } = options;\n\n if (ATTACHED.has(renderer)) {\n throw new Error(\n \"three-meter: a PerformanceMonitor is already attached to this renderer. dispose() it first.\",\n );\n }\n\n ATTACHED.add(renderer);\n this.renderer = renderer;\n renderer.info.autoReset = false;\n this.originalRender = renderer.render;\n\n renderer.render = (...args: never[]) => {\n this.renderPasses += 1;\n const callsBefore = frameCalls(renderer.info);\n\n try {\n return this.originalRender.apply(renderer, args);\n } finally {\n this.lastRenderEndAt = performance.now();\n this.notePass(args[0], args[1], frameCalls(renderer.info) - callsBefore);\n }\n };\n\n this.gpuTimer = trackGPU ? GpuTimer.create(renderer, gpuQueryPoolSize) : null;\n this.fpsHistory = new RingBuffer(historySize);\n this.cpuHistory = new RingBuffer(historySize);\n this.gpuHistory = new RingBuffer(historySize);\n this.frameIntervals = new RingBuffer(frameStatsSize);\n this.sample = this.buildEmptySample();\n }\n\n begin() {\n const now = performance.now();\n\n if (this.lastBeginAt !== null) {\n const frameMs = now - this.lastBeginAt;\n\n // A stall keeps the previous reading rather than logging a near-zero frame.\n if (frameMs > 0 && frameMs <= STALL_MS) {\n this.frameFps = 1000 / frameMs;\n this.frameIntervals.push(frameMs);\n this.frameStats = null;\n }\n }\n\n this.lastBeginAt = now;\n this.cpuStart = now;\n this.lastRenderEndAt = null;\n this.renderPasses = 0;\n this.mainPassCalls = -1;\n this.renderer.info.reset();\n this.gpuTimer?.begin();\n }\n\n end() {\n const now = performance.now();\n // Without a render this frame there is nothing better than \"until end()\".\n const cpu = (this.lastRenderEndAt ?? now) - this.cpuStart;\n\n this.gpuTimer?.end();\n this.gpuTimer?.poll();\n\n const gpuMs = this.gpuTimer?.getMs() ?? 0;\n\n this.smoothedFps =\n this.smoothedFps === 0\n ? this.frameFps\n : this.smoothedFps + (this.frameFps - this.smoothedFps) * FPS_SMOOTHING;\n\n this.fpsHistory.push(this.frameFps);\n this.cpuHistory.push(cpu);\n this.gpuHistory.push(gpuMs);\n\n const info = this.renderer.info;\n\n this.sample = Object.freeze({\n cpu,\n fps: this.smoothedFps,\n gpu: { available: this.gpuTimer !== null, ms: gpuMs },\n render: {\n calls: frameCalls(info),\n lines: info.render.lines,\n passes: this.renderPasses,\n points: info.render.points,\n triangles: info.render.triangles,\n },\n resources: {\n geometries: info.memory.geometries,\n programs: programCount(info),\n textures: info.memory.textures,\n },\n });\n }\n\n getSample(): Sample {\n return this.sample;\n }\n\n getHistory(metric: TimingMetric): readonly number[] {\n switch (metric) {\n case \"fps\":\n return this.fpsHistory.toArray();\n case \"cpu\":\n return this.cpuHistory.toArray();\n case \"gpu\":\n return this.gpuHistory.toArray();\n }\n }\n\n /** 1% low, p99 frame time and hitches. Computed on read, cached until the next frame. */\n getFrameStats(): FrameStats {\n this.frameStats ??= computeFrameStats(this.frameIntervals.toArray());\n\n return this.frameStats;\n }\n\n /**\n * Where the main pass's draw calls and triangles come from, by mesh and by\n * material. Walks the scene graph, so read it on demand (the HUD does, at\n * 2 Hz while its section is open), not every frame. `null` before the first\n * render, or once the scene is garbage collected.\n */\n getSceneCost(): SceneCost | null {\n const scene = this.mainPass?.scene.deref();\n const camera = this.mainPass?.camera.deref();\n\n return scene && camera ? computeSceneCost(scene, camera) : null;\n }\n\n /**\n * Every visible object in the main scene behind a {@link CostEntry}'s `key`,\n * including ones outside the camera, which `getSceneCost()` leaves out.\n */\n getObjectsForKey(key: string): unknown[] {\n const scene = this.mainPass?.scene.deref();\n\n return scene ? objectsForKey(scene, key) : [];\n }\n\n /**\n * three revision, backend and GPU name. Cached once the backend is known;\n * before `WebGPURenderer.init()` settles it, `backend` is `null` and the\n * next call reads again.\n */\n getEnvironment(): Environment {\n if (this.environment) {\n return this.environment;\n }\n\n const environment = Object.freeze(readEnvironment(this.renderer));\n\n if (environment.backend !== null) {\n this.environment = environment;\n }\n\n return environment;\n }\n\n dispose() {\n ATTACHED.delete(this.renderer);\n this.renderer.info.autoReset = true;\n this.renderer.render = this.originalRender;\n this.gpuTimer?.dispose();\n this.fpsHistory.clear();\n this.cpuHistory.clear();\n this.gpuHistory.clear();\n this.frameIntervals.clear();\n this.frameStats = null;\n this.mainPass = null;\n }\n\n /**\n * The main pass is the render call that drew the most, so a post-processing\n * quad rendered last doesn't stand in for the scene. Weak references: the\n * monitor never keeps a scene alive.\n */\n private notePass(scene: unknown, camera: unknown, calls: number) {\n if (\n calls > this.mainPassCalls &&\n typeof scene === \"object\" &&\n scene !== null &&\n typeof camera === \"object\" &&\n camera !== null\n ) {\n this.mainPassCalls = calls;\n this.mainPass = { camera: new WeakRef(camera), scene: new WeakRef(scene) };\n }\n }\n\n private buildEmptySample(): Sample {\n return Object.freeze({\n cpu: 0,\n fps: 0,\n gpu: { available: this.gpuTimer !== null, ms: 0 },\n render: { calls: 0, lines: 0, passes: 0, points: 0, triangles: 0 },\n resources: { geometries: 0, programs: 0, textures: 0 },\n });\n }\n}\n\nexport { PerformanceMonitor };\n"],"mappings":"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"}
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { s as PerformanceMonitorOptions, t as PerformanceMonitor } from "./performance-monitor-DgHxeQ2X.js";
2
- import { t as MountPerfHudOptions } from "./mount-perf-hud-BKpk62Pp.js";
1
+ import { s as PerformanceMonitorOptions, t as PerformanceMonitor } from "./performance-monitor-BoY_-GTt.js";
2
+ import { t as MountPerfHudOptions } from "./mount-perf-hud-MTPYNhG9.js";
3
3
  //#region src/react/monitor-store.d.ts
4
4
  type MonitorListener = (monitor: PerformanceMonitor | null) => void;
5
5
  type PerfMonitorStore = {
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as PerformanceMonitor } from "./performance-monitor-DsBp13ts.js";
2
- import { t as mountPerfHud } from "./mount-perf-hud-CebnfB8t.js";
1
+ import { t as PerformanceMonitor } from "./performance-monitor-BqnoiLEs.js";
2
+ import { t as mountPerfHud } from "./mount-perf-hud-U_Xlj0Od.js";
3
3
  import { useEffect, useRef } from "react";
4
4
  import { useFrame, useThree } from "@react-three/fiber";
5
5
  //#region src/react/monitor-store.ts
package/dist/styles.css CHANGED
@@ -232,11 +232,10 @@
232
232
  }
233
233
 
234
234
  .perf-monitor__cost {
235
- /* Rows are buttons, which shrink to their content without a width. */
236
235
  box-sizing: border-box;
237
236
  width: 100%;
238
237
  display: grid;
239
- grid-template-columns: minmax(0, 1fr) 5ch 6ch;
238
+ grid-template-columns: minmax(0, 1fr) 5ch 6ch 14px;
240
239
  gap: 6px;
241
240
  align-items: baseline;
242
241
  padding: 2px 6px;
@@ -246,13 +245,58 @@
246
245
  color: inherit;
247
246
  font: inherit;
248
247
  text-align: left;
249
- cursor: pointer;
248
+ cursor: default;
250
249
  }
251
250
 
252
251
  .perf-monitor__cost:not(.perf-monitor__cost--head):hover {
253
252
  background: var(--perf-row);
254
253
  }
255
254
 
255
+ .perf-monitor__cost.is-hidden .perf-monitor__cost-name {
256
+ opacity: 0.6;
257
+ text-decoration: line-through;
258
+ }
259
+
260
+ /* What hiding saved, across the calls and tris columns. */
261
+ .perf-monitor__cost-saved {
262
+ grid-column: 2 / span 2;
263
+ color: var(--perf-accent);
264
+ font-variant-numeric: tabular-nums lining-nums;
265
+ text-align: right;
266
+ white-space: nowrap;
267
+ }
268
+
269
+ .perf-monitor__cost-eye {
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ width: 14px;
274
+ height: 14px;
275
+ padding: 0;
276
+ border: 0;
277
+ border-radius: 3px;
278
+ background: transparent;
279
+ color: var(--perf-muted);
280
+ cursor: pointer;
281
+ align-self: center;
282
+ }
283
+
284
+ .perf-monitor__cost-eye .perf-monitor__icon {
285
+ width: 12px;
286
+ height: 12px;
287
+ color: inherit;
288
+ }
289
+
290
+ .perf-monitor__cost-eye:hover,
291
+ .perf-monitor__cost-eye[aria-pressed="true"] {
292
+ color: var(--perf-fg);
293
+ }
294
+
295
+ .perf-monitor__cost-eye:focus-visible {
296
+ outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
297
+ outline-offset: 1px;
298
+ }
299
+
256
300
  .perf-monitor__cost:focus-visible {
257
301
  outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
258
302
  outline-offset: -1px;
@@ -263,13 +307,30 @@
263
307
  cursor: default;
264
308
  }
265
309
 
310
+ /* The name is a button: a click logs the row's objects. */
266
311
  .perf-monitor__cost-name {
312
+ min-width: 0;
313
+ padding: 0;
314
+ border: 0;
315
+ background: transparent;
316
+ font: inherit;
317
+ text-align: left;
318
+ cursor: pointer;
267
319
  overflow: hidden;
268
320
  text-overflow: ellipsis;
269
321
  white-space: nowrap;
270
322
  color: var(--perf-fg-dim);
271
323
  }
272
324
 
325
+ .perf-monitor__cost-name:hover {
326
+ color: var(--perf-fg);
327
+ }
328
+
329
+ .perf-monitor__cost-name:focus-visible {
330
+ outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);
331
+ outline-offset: 1px;
332
+ }
333
+
273
334
  .perf-monitor__cost-copies {
274
335
  color: var(--perf-muted);
275
336
  }
package/dist/ui.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { t as PerformanceMonitor } from "./performance-monitor-DgHxeQ2X.js";
2
- import { C as dockPanel, D as resolveBudgets, E as ResolvedBudgets, S as ScreenEdge, T as Budgets, _ as DefaultPlacement, a as PerformanceViewMode, b as DockPanelOptions, c as HudDefaults, d as HudSettingsOptions, f as HudTheme, g as isThemeMode, h as ThemeMode, i as PerformanceView, l as HudSelection, m as THEME_MODES, n as PerfHudHandle, o as PerformanceViewOptions, p as ResolvedTheme, r as mountPerfHud, s as DEFAULT_STORAGE_KEY, t as MountPerfHudOptions, u as HudSettings, v as DockAlign, w as BudgetKey, x as Placement, y as DockHandle } from "./mount-perf-hud-BKpk62Pp.js";
1
+ import { t as PerformanceMonitor } from "./performance-monitor-BoY_-GTt.js";
2
+ import { C as dockPanel, D as resolveBudgets, E as ResolvedBudgets, S as ScreenEdge, T as Budgets, _ as DefaultPlacement, a as PerformanceViewMode, b as DockPanelOptions, c as HudDefaults, d as HudSettingsOptions, f as HudTheme, g as isThemeMode, h as ThemeMode, i as PerformanceView, l as HudSelection, m as THEME_MODES, n as PerfHudHandle, o as PerformanceViewOptions, p as ResolvedTheme, r as mountPerfHud, s as DEFAULT_STORAGE_KEY, t as MountPerfHudOptions, u as HudSettings, v as DockAlign, w as BudgetKey, x as Placement, y as DockHandle } from "./mount-perf-hud-MTPYNhG9.js";
3
3
  //#region src/ui/report.d.ts
4
4
  /** The report for `monitor` right now, as Markdown ready to paste into an issue. */
5
5
  declare const formatReport: (monitor: PerformanceMonitor) => string;
6
6
  //#endregion
7
7
  //#region src/ui/styles.d.ts
8
- declare const PERF_HUD_STYLES = "\n.perf-hud,\n.perf-monitor {\n --perf-bg: rgba(22, 24, 29, 0.85);\n --perf-fg: #e6e8eb;\n --perf-fg-dim: #c5c8ce;\n --perf-muted: #8b909a;\n --perf-row: rgba(255, 255, 255, 0.04);\n --perf-border: rgba(255, 255, 255, 0.12);\n --perf-accent: #60a5fa;\n --perf-warn: #f59e0b;\n --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);\n\n color-scheme: dark;\n}\n\n.perf-hud[data-theme=\"light\"],\n.perf-monitor[data-theme=\"light\"] {\n --perf-bg: rgba(250, 250, 252, 0.88);\n --perf-fg: #1a1c21;\n --perf-fg-dim: #4b5058;\n --perf-muted: #6b7079;\n --perf-row: rgba(0, 0, 0, 0.05);\n --perf-border: rgba(0, 0, 0, 0.12);\n --perf-accent: #2563eb;\n --perf-warn: #b45309;\n --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);\n\n color-scheme: light;\n}\n\n.perf-monitor {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 8px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.4;\n color: var(--perf-fg);\n}\n\n.perf-monitor__graphs {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.perf-monitor__graph {\n position: relative;\n height: 40px;\n background: var(--perf-row);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.perf-monitor__canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.perf-monitor__graph-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 8px;\n pointer-events: none;\n}\n\n.perf-monitor__graph-head {\n display: flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n}\n\n.perf-monitor__graph-overlay--toggle {\n pointer-events: auto;\n cursor: pointer;\n}\n\n.perf-monitor__graph-label {\n color: var(--perf-muted);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n font-size: 10px;\n}\n\n.perf-monitor__graph-value {\n min-width: 6ch;\n font-weight: 600;\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.perf-monitor__row {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 6px;\n border-radius: 3px;\n cursor: pointer;\n}\n\n/* Hidden until \"explain metrics\" is on; then the row wraps it under the label. */\n.perf-monitor__hint {\n display: none;\n flex-basis: 100%;\n padding: 0 0 2px 18px;\n color: var(--perf-muted);\n font-size: 10px;\n line-height: 1.35;\n opacity: 0.85;\n}\n\n.perf-monitor--explain .perf-monitor__hint {\n display: block;\n}\n\n.perf-monitor--explain .perf-monitor__section .perf-monitor__row {\n flex-wrap: wrap;\n}\n\n.perf-monitor__row:nth-child(even) {\n background: var(--perf-row);\n}\n\n.perf-monitor__label {\n flex: 1;\n color: var(--perf-muted);\n}\n\n.perf-monitor__icon {\n flex: none;\n width: 12px;\n height: 12px;\n color: var(--perf-muted);\n}\n\n.perf-monitor__options {\n padding-bottom: 8px;\n border-bottom: 1px solid var(--perf-border);\n}\n\n.perf-monitor__row--static {\n cursor: default;\n}\n\n/* Light / system / dark, as a small segmented radio group. */\n.perf-monitor__segment {\n display: inline-flex;\n flex: none;\n gap: 1px;\n padding: 1px;\n border-radius: 4px;\n background: var(--perf-row);\n}\n\n.perf-monitor__segment-option {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 16px;\n padding: 0;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: var(--perf-muted);\n cursor: pointer;\n}\n\n.perf-monitor__segment-option:hover {\n color: var(--perf-fg);\n}\n\n.perf-monitor__segment-option[aria-checked=\"true\"] {\n color: var(--perf-fg);\n background: var(--perf-border);\n}\n\n.perf-monitor__segment-option:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n.perf-monitor__segment-option .perf-monitor__icon {\n width: 11px;\n height: 11px;\n color: inherit;\n}\n\n.perf-monitor__button {\n flex: none;\n min-width: 7ch;\n padding: 0 6px;\n border: 1px solid var(--perf-border);\n border-radius: 3px;\n background: var(--perf-row);\n color: var(--perf-fg-dim);\n font: inherit;\n cursor: pointer;\n}\n\n.perf-monitor__button:hover {\n color: var(--perf-fg);\n}\n\n.perf-monitor__button:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n/* Top costs: name, calls, triangles. Rows are buttons that log their objects. */\n.perf-monitor__costs-body {\n display: flex;\n flex-direction: column;\n gap: 1px;\n padding-top: 2px;\n}\n\n.perf-monitor__costs-body[hidden] {\n display: none;\n}\n\n.perf-monitor__cost {\n /* Rows are buttons, which shrink to their content without a width. */\n box-sizing: border-box;\n width: 100%;\n display: grid;\n grid-template-columns: minmax(0, 1fr) 5ch 6ch;\n gap: 6px;\n align-items: baseline;\n padding: 2px 6px;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.perf-monitor__cost:not(.perf-monitor__cost--head):hover {\n background: var(--perf-row);\n}\n\n.perf-monitor__cost:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: -1px;\n}\n\n.perf-monitor__cost--head {\n align-items: center;\n cursor: default;\n}\n\n.perf-monitor__cost-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--perf-fg-dim);\n}\n\n.perf-monitor__cost-copies {\n color: var(--perf-muted);\n}\n\n.perf-monitor__cost-value {\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__cost-sort {\n padding: 0;\n border: 0;\n background: transparent;\n color: var(--perf-muted);\n font: inherit;\n font-size: 10px;\n text-align: right;\n cursor: pointer;\n}\n\n.perf-monitor__cost-sort[aria-pressed=\"true\"] {\n color: var(--perf-fg);\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n.perf-monitor__segment-option--text {\n width: auto;\n padding: 0 5px;\n font: inherit;\n font-size: 10px;\n}\n\n.perf-monitor__costs-note {\n padding: 2px 6px;\n color: var(--perf-muted);\n font-size: 10px;\n}\n\n.perf-monitor__checkbox {\n flex: none;\n margin: 0;\n width: 12px;\n height: 12px;\n accent-color: var(--perf-accent);\n cursor: pointer;\n}\n\n.perf-monitor__value {\n min-width: 7ch;\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__hud {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 2px;\n}\n\n.perf-monitor__hud-graphs {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.perf-monitor__hud-graphs:empty {\n display: none;\n}\n\n.perf-monitor__hud-graph {\n position: relative;\n height: 28px;\n background: var(--perf-row);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.perf-monitor__hud-grid {\n display: grid;\n grid-template-columns: auto 6ch auto 6ch;\n align-items: baseline;\n gap: 2px 8px;\n}\n\n.perf-monitor__hud-grid:empty {\n display: none;\n}\n\n.perf-monitor__hud-item {\n display: contents;\n}\n\n.perf-monitor__hud-label {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--perf-muted);\n font-size: 10px;\n}\n\n.perf-monitor__hud-label .perf-monitor__icon,\n.perf-monitor__hud-graph .perf-monitor__icon {\n width: 11px;\n height: 11px;\n}\n\n.perf-monitor__hud-value {\n font-weight: 600;\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__footer {\n display: flex;\n align-items: flex-start;\n gap: 6px;\n padding: 8px 6px 0;\n border-top: 1px solid var(--perf-border);\n color: var(--perf-muted);\n font-size: 10px;\n}\n\n.perf-monitor__footer-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n\n.perf-monitor__footer-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-width: 0;\n}\n\n.perf-monitor__footer-row[hidden],\n.perf-monitor__badge[hidden] {\n display: none;\n}\n\n.perf-monitor__footer-gpu {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--perf-fg-dim);\n}\n\n.perf-monitor__badge {\n flex: none;\n padding: 0 5px;\n border: 1px solid var(--perf-border);\n border-radius: 3px;\n color: var(--perf-fg-dim);\n letter-spacing: 0.04em;\n}\n\n.perf-monitor__badge.is-fallback {\n border-color: var(--perf-warn);\n color: var(--perf-warn);\n}\n\n/* Past its budget. The value's tooltip names the budget. */\n.perf-monitor__value.is-over,\n.perf-monitor__hud-value.is-over,\n.perf-monitor__graph-value.is-over {\n color: var(--perf-warn);\n}\n\n.perf-monitor__link {\n color: inherit;\n text-decoration: none;\n}\n\n.perf-monitor__link:hover,\n.perf-monitor__link:focus-visible {\n color: var(--perf-accent);\n text-decoration: underline;\n}\n\n.perf-monitor--full .perf-monitor__hud {\n display: none;\n}\n\n.perf-monitor--compact .perf-monitor__graphs,\n.perf-monitor--compact .perf-monitor__section,\n.perf-monitor--compact:not(.perf-monitor--info) .perf-monitor__footer,\n.perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox,\n.perf-monitor--compact .perf-monitor__footer > .perf-monitor__icon {\n display: none;\n}\n\n.perf-monitor--compact .perf-monitor__footer {\n padding: 6px 2px 2px;\n}\n\n.perf-monitor--compact {\n padding: 4px 8px;\n}\n\n.perf-hud {\n position: fixed;\n z-index: 9999;\n /* Safari resolves width:100% canvases against the viewport on a fixed\n host with auto width. Shrink-wrap so expand stays a card, not the window. */\n width: max-content;\n max-width: min(20rem, calc(100vw - 48px));\n overflow: visible;\n background: transparent;\n pointer-events: none;\n user-select: none;\n touch-action: none;\n}\n\n.perf-hud--full > .perf-monitor {\n width: 16rem;\n max-width: 100%;\n}\n\n/* Room for the hints, so most fit on two or three lines. Plus padding, this fills the host's 20rem cap. */\n.perf-hud--full > .perf-monitor--explain {\n width: 19rem;\n}\n\n.perf-hud > .perf-monitor {\n position: relative;\n z-index: 1;\n max-height: 70vh;\n overflow: auto;\n background: var(--perf-bg);\n border-radius: 6px;\n box-shadow: var(--perf-shadow);\n backdrop-filter: blur(2px);\n pointer-events: auto;\n transition: opacity 0.25s ease;\n}\n\n.perf-hud--dim:not(.is-awake):not(.is-dragging) > .perf-monitor {\n opacity: 0.32;\n}\n\n.perf-hud__hotspot {\n position: absolute;\n z-index: 0;\n inset: -20px;\n pointer-events: auto;\n}\n\n.perf-hud:not([data-edge=\"right\"]) .perf-hud__hotspot {\n right: -46px;\n}\n\n.perf-hud[data-edge=\"right\"] .perf-hud__hotspot {\n left: -46px;\n}\n\n.perf-hud__tools {\n position: absolute;\n top: 0;\n z-index: 2;\n display: flex;\n flex-direction: column;\n gap: 4px;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n}\n\n.perf-hud.is-awake .perf-hud__tools,\n.perf-hud.is-dragging .perf-hud__tools {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Inward side: right by default, left when docked to the right edge. */\n.perf-hud:not([data-edge=\"right\"]) .perf-hud__tools {\n left: calc(100% + 6px);\n}\n\n.perf-hud[data-edge=\"right\"] .perf-hud__tools {\n right: calc(100% + 6px);\n}\n\n.perf-hud__disc {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: 0;\n border-radius: 999px;\n background: var(--perf-bg);\n box-shadow: var(--perf-shadow);\n color: var(--perf-fg-dim);\n cursor: pointer;\n backdrop-filter: blur(2px);\n}\n\n.perf-hud__icon {\n width: 12px;\n height: 12px;\n}\n\n.perf-hud__drag {\n cursor: grab;\n}\n\n.perf-hud.is-dragging .perf-hud__drag {\n cursor: grabbing;\n}\n\n/* The ring is only ever hover or keyboard focus, never state. */\n.perf-hud__disc:hover,\n.perf-hud__disc:focus-visible {\n color: var(--perf-fg);\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n/* Dim on is a solid accent disc, off a plain one, so the state reads without hovering and\n hover keeps its own look on top of either. After the hover rule, so on stays on under it. */\n.perf-hud__dim[aria-pressed=\"true\"] {\n background: var(--perf-accent);\n color: var(--perf-bg);\n}\n";
8
+ declare const PERF_HUD_STYLES = "\n.perf-hud,\n.perf-monitor {\n --perf-bg: rgba(22, 24, 29, 0.85);\n --perf-fg: #e6e8eb;\n --perf-fg-dim: #c5c8ce;\n --perf-muted: #8b909a;\n --perf-row: rgba(255, 255, 255, 0.04);\n --perf-border: rgba(255, 255, 255, 0.12);\n --perf-accent: #60a5fa;\n --perf-warn: #f59e0b;\n --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);\n\n color-scheme: dark;\n}\n\n.perf-hud[data-theme=\"light\"],\n.perf-monitor[data-theme=\"light\"] {\n --perf-bg: rgba(250, 250, 252, 0.88);\n --perf-fg: #1a1c21;\n --perf-fg-dim: #4b5058;\n --perf-muted: #6b7079;\n --perf-row: rgba(0, 0, 0, 0.05);\n --perf-border: rgba(0, 0, 0, 0.12);\n --perf-accent: #2563eb;\n --perf-warn: #b45309;\n --perf-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);\n\n color-scheme: light;\n}\n\n.perf-monitor {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 8px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.4;\n color: var(--perf-fg);\n}\n\n.perf-monitor__graphs {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.perf-monitor__graph {\n position: relative;\n height: 40px;\n background: var(--perf-row);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.perf-monitor__canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.perf-monitor__graph-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 8px;\n pointer-events: none;\n}\n\n.perf-monitor__graph-head {\n display: flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n}\n\n.perf-monitor__graph-overlay--toggle {\n pointer-events: auto;\n cursor: pointer;\n}\n\n.perf-monitor__graph-label {\n color: var(--perf-muted);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n font-size: 10px;\n}\n\n.perf-monitor__graph-value {\n min-width: 6ch;\n font-weight: 600;\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.perf-monitor__row {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 6px;\n border-radius: 3px;\n cursor: pointer;\n}\n\n/* Hidden until \"explain metrics\" is on; then the row wraps it under the label. */\n.perf-monitor__hint {\n display: none;\n flex-basis: 100%;\n padding: 0 0 2px 18px;\n color: var(--perf-muted);\n font-size: 10px;\n line-height: 1.35;\n opacity: 0.85;\n}\n\n.perf-monitor--explain .perf-monitor__hint {\n display: block;\n}\n\n.perf-monitor--explain .perf-monitor__section .perf-monitor__row {\n flex-wrap: wrap;\n}\n\n.perf-monitor__row:nth-child(even) {\n background: var(--perf-row);\n}\n\n.perf-monitor__label {\n flex: 1;\n color: var(--perf-muted);\n}\n\n.perf-monitor__icon {\n flex: none;\n width: 12px;\n height: 12px;\n color: var(--perf-muted);\n}\n\n.perf-monitor__options {\n padding-bottom: 8px;\n border-bottom: 1px solid var(--perf-border);\n}\n\n.perf-monitor__row--static {\n cursor: default;\n}\n\n/* Light / system / dark, as a small segmented radio group. */\n.perf-monitor__segment {\n display: inline-flex;\n flex: none;\n gap: 1px;\n padding: 1px;\n border-radius: 4px;\n background: var(--perf-row);\n}\n\n.perf-monitor__segment-option {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 16px;\n padding: 0;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: var(--perf-muted);\n cursor: pointer;\n}\n\n.perf-monitor__segment-option:hover {\n color: var(--perf-fg);\n}\n\n.perf-monitor__segment-option[aria-checked=\"true\"] {\n color: var(--perf-fg);\n background: var(--perf-border);\n}\n\n.perf-monitor__segment-option:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n.perf-monitor__segment-option .perf-monitor__icon {\n width: 11px;\n height: 11px;\n color: inherit;\n}\n\n.perf-monitor__button {\n flex: none;\n min-width: 7ch;\n padding: 0 6px;\n border: 1px solid var(--perf-border);\n border-radius: 3px;\n background: var(--perf-row);\n color: var(--perf-fg-dim);\n font: inherit;\n cursor: pointer;\n}\n\n.perf-monitor__button:hover {\n color: var(--perf-fg);\n}\n\n.perf-monitor__button:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n/* Top costs: name, calls, triangles. Rows are buttons that log their objects. */\n.perf-monitor__costs-body {\n display: flex;\n flex-direction: column;\n gap: 1px;\n padding-top: 2px;\n}\n\n.perf-monitor__costs-body[hidden] {\n display: none;\n}\n\n.perf-monitor__cost {\n box-sizing: border-box;\n width: 100%;\n display: grid;\n grid-template-columns: minmax(0, 1fr) 5ch 6ch 14px;\n gap: 6px;\n align-items: baseline;\n padding: 2px 6px;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: default;\n}\n\n.perf-monitor__cost:not(.perf-monitor__cost--head):hover {\n background: var(--perf-row);\n}\n\n.perf-monitor__cost.is-hidden .perf-monitor__cost-name {\n opacity: 0.6;\n text-decoration: line-through;\n}\n\n/* What hiding saved, across the calls and tris columns. */\n.perf-monitor__cost-saved {\n grid-column: 2 / span 2;\n color: var(--perf-accent);\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n white-space: nowrap;\n}\n\n.perf-monitor__cost-eye {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n padding: 0;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: var(--perf-muted);\n cursor: pointer;\n align-self: center;\n}\n\n.perf-monitor__cost-eye .perf-monitor__icon {\n width: 12px;\n height: 12px;\n color: inherit;\n}\n\n.perf-monitor__cost-eye:hover,\n.perf-monitor__cost-eye[aria-pressed=\"true\"] {\n color: var(--perf-fg);\n}\n\n.perf-monitor__cost-eye:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n.perf-monitor__cost:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: -1px;\n}\n\n.perf-monitor__cost--head {\n align-items: center;\n cursor: default;\n}\n\n/* The name is a button: a click logs the row's objects. */\n.perf-monitor__cost-name {\n min-width: 0;\n padding: 0;\n border: 0;\n background: transparent;\n font: inherit;\n text-align: left;\n cursor: pointer;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--perf-fg-dim);\n}\n\n.perf-monitor__cost-name:hover {\n color: var(--perf-fg);\n}\n\n.perf-monitor__cost-name:focus-visible {\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n.perf-monitor__cost-copies {\n color: var(--perf-muted);\n}\n\n.perf-monitor__cost-value {\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__cost-sort {\n padding: 0;\n border: 0;\n background: transparent;\n color: var(--perf-muted);\n font: inherit;\n font-size: 10px;\n text-align: right;\n cursor: pointer;\n}\n\n.perf-monitor__cost-sort[aria-pressed=\"true\"] {\n color: var(--perf-fg);\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n.perf-monitor__segment-option--text {\n width: auto;\n padding: 0 5px;\n font: inherit;\n font-size: 10px;\n}\n\n.perf-monitor__costs-note {\n padding: 2px 6px;\n color: var(--perf-muted);\n font-size: 10px;\n}\n\n.perf-monitor__checkbox {\n flex: none;\n margin: 0;\n width: 12px;\n height: 12px;\n accent-color: var(--perf-accent);\n cursor: pointer;\n}\n\n.perf-monitor__value {\n min-width: 7ch;\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__hud {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 2px;\n}\n\n.perf-monitor__hud-graphs {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.perf-monitor__hud-graphs:empty {\n display: none;\n}\n\n.perf-monitor__hud-graph {\n position: relative;\n height: 28px;\n background: var(--perf-row);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.perf-monitor__hud-grid {\n display: grid;\n grid-template-columns: auto 6ch auto 6ch;\n align-items: baseline;\n gap: 2px 8px;\n}\n\n.perf-monitor__hud-grid:empty {\n display: none;\n}\n\n.perf-monitor__hud-item {\n display: contents;\n}\n\n.perf-monitor__hud-label {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--perf-muted);\n font-size: 10px;\n}\n\n.perf-monitor__hud-label .perf-monitor__icon,\n.perf-monitor__hud-graph .perf-monitor__icon {\n width: 11px;\n height: 11px;\n}\n\n.perf-monitor__hud-value {\n font-weight: 600;\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n\n.perf-monitor__footer {\n display: flex;\n align-items: flex-start;\n gap: 6px;\n padding: 8px 6px 0;\n border-top: 1px solid var(--perf-border);\n color: var(--perf-muted);\n font-size: 10px;\n}\n\n.perf-monitor__footer-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n\n.perf-monitor__footer-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-width: 0;\n}\n\n.perf-monitor__footer-row[hidden],\n.perf-monitor__badge[hidden] {\n display: none;\n}\n\n.perf-monitor__footer-gpu {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--perf-fg-dim);\n}\n\n.perf-monitor__badge {\n flex: none;\n padding: 0 5px;\n border: 1px solid var(--perf-border);\n border-radius: 3px;\n color: var(--perf-fg-dim);\n letter-spacing: 0.04em;\n}\n\n.perf-monitor__badge.is-fallback {\n border-color: var(--perf-warn);\n color: var(--perf-warn);\n}\n\n/* Past its budget. The value's tooltip names the budget. */\n.perf-monitor__value.is-over,\n.perf-monitor__hud-value.is-over,\n.perf-monitor__graph-value.is-over {\n color: var(--perf-warn);\n}\n\n.perf-monitor__link {\n color: inherit;\n text-decoration: none;\n}\n\n.perf-monitor__link:hover,\n.perf-monitor__link:focus-visible {\n color: var(--perf-accent);\n text-decoration: underline;\n}\n\n.perf-monitor--full .perf-monitor__hud {\n display: none;\n}\n\n.perf-monitor--compact .perf-monitor__graphs,\n.perf-monitor--compact .perf-monitor__section,\n.perf-monitor--compact:not(.perf-monitor--info) .perf-monitor__footer,\n.perf-monitor--compact .perf-monitor__footer > .perf-monitor__checkbox,\n.perf-monitor--compact .perf-monitor__footer > .perf-monitor__icon {\n display: none;\n}\n\n.perf-monitor--compact .perf-monitor__footer {\n padding: 6px 2px 2px;\n}\n\n.perf-monitor--compact {\n padding: 4px 8px;\n}\n\n.perf-hud {\n position: fixed;\n z-index: 9999;\n /* Safari resolves width:100% canvases against the viewport on a fixed\n host with auto width. Shrink-wrap so expand stays a card, not the window. */\n width: max-content;\n max-width: min(20rem, calc(100vw - 48px));\n overflow: visible;\n background: transparent;\n pointer-events: none;\n user-select: none;\n touch-action: none;\n}\n\n.perf-hud--full > .perf-monitor {\n width: 16rem;\n max-width: 100%;\n}\n\n/* Room for the hints, so most fit on two or three lines. Plus padding, this fills the host's 20rem cap. */\n.perf-hud--full > .perf-monitor--explain {\n width: 19rem;\n}\n\n.perf-hud > .perf-monitor {\n position: relative;\n z-index: 1;\n max-height: 70vh;\n overflow: auto;\n background: var(--perf-bg);\n border-radius: 6px;\n box-shadow: var(--perf-shadow);\n backdrop-filter: blur(2px);\n pointer-events: auto;\n transition: opacity 0.25s ease;\n}\n\n.perf-hud--dim:not(.is-awake):not(.is-dragging) > .perf-monitor {\n opacity: 0.32;\n}\n\n.perf-hud__hotspot {\n position: absolute;\n z-index: 0;\n inset: -20px;\n pointer-events: auto;\n}\n\n.perf-hud:not([data-edge=\"right\"]) .perf-hud__hotspot {\n right: -46px;\n}\n\n.perf-hud[data-edge=\"right\"] .perf-hud__hotspot {\n left: -46px;\n}\n\n.perf-hud__tools {\n position: absolute;\n top: 0;\n z-index: 2;\n display: flex;\n flex-direction: column;\n gap: 4px;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n}\n\n.perf-hud.is-awake .perf-hud__tools,\n.perf-hud.is-dragging .perf-hud__tools {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Inward side: right by default, left when docked to the right edge. */\n.perf-hud:not([data-edge=\"right\"]) .perf-hud__tools {\n left: calc(100% + 6px);\n}\n\n.perf-hud[data-edge=\"right\"] .perf-hud__tools {\n right: calc(100% + 6px);\n}\n\n.perf-hud__disc {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: 0;\n border-radius: 999px;\n background: var(--perf-bg);\n box-shadow: var(--perf-shadow);\n color: var(--perf-fg-dim);\n cursor: pointer;\n backdrop-filter: blur(2px);\n}\n\n.perf-hud__icon {\n width: 12px;\n height: 12px;\n}\n\n.perf-hud__drag {\n cursor: grab;\n}\n\n.perf-hud.is-dragging .perf-hud__drag {\n cursor: grabbing;\n}\n\n/* The ring is only ever hover or keyboard focus, never state. */\n.perf-hud__disc:hover,\n.perf-hud__disc:focus-visible {\n color: var(--perf-fg);\n outline: 1px solid color-mix(in srgb, var(--perf-accent) 60%, transparent);\n outline-offset: 1px;\n}\n\n/* Dim on is a solid accent disc, off a plain one, so the state reads without hovering and\n hover keeps its own look on top of either. After the hover rule, so on stays on under it. */\n.perf-hud__dim[aria-pressed=\"true\"] {\n background: var(--perf-accent);\n color: var(--perf-bg);\n}\n";
9
9
  /**
10
10
  * Append the stylesheet to `target`'s head, once. Call before mounting a view
11
11
  * by hand; {@link mountPerfHud} does it for you unless told not to.
package/dist/ui.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as formatReport, c as HudSettings, d as isThemeMode, f as resolveBudgets, i as PerformanceView, l as HudTheme, n as PERF_HUD_STYLES, o as dockPanel, r as injectStyles, s as DEFAULT_STORAGE_KEY, t as mountPerfHud, u as THEME_MODES } from "./mount-perf-hud-CebnfB8t.js";
1
+ import { a as formatReport, c as HudSettings, d as isThemeMode, f as resolveBudgets, i as PerformanceView, l as HudTheme, n as PERF_HUD_STYLES, o as dockPanel, r as injectStyles, s as DEFAULT_STORAGE_KEY, t as mountPerfHud, u as THEME_MODES } from "./mount-perf-hud-U_Xlj0Od.js";
2
2
  export { DEFAULT_STORAGE_KEY, HudSettings, HudTheme, PERF_HUD_STYLES, PerformanceView, THEME_MODES, dockPanel, formatReport, injectStyles, isThemeMode, mountPerfHud, resolveBudgets };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkmake/three-meter",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Zero-dependency three.js frame metrics (FPS, CPU, GPU, draw calls) with a dockable HUD. Vanilla three and React Three Fiber adapters.",
5
5
  "keywords": [
6
6
  "devtools",
@@ -14,13 +14,14 @@
14
14
  "three",
15
15
  "threejs"
16
16
  ],
17
- "homepage": "https://github.com/zkmake/three-meter#readme",
18
- "bugs": "https://github.com/zkmake/three-meter/issues",
17
+ "homepage": "https://github.com/zkmake/three-kit/tree/main/packages/three-meter#readme",
18
+ "bugs": "https://github.com/zkmake/three-kit/issues",
19
19
  "license": "MIT",
20
20
  "author": "Zubin Khavarian",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/zkmake/three-meter.git"
23
+ "url": "git+https://github.com/zkmake/three-kit.git",
24
+ "directory": "packages/three-meter"
24
25
  },
25
26
  "files": [
26
27
  "dist",
@@ -41,34 +42,23 @@
41
42
  },
42
43
  "scripts": {
43
44
  "build": "tsdown",
44
- "changeset": "changeset",
45
- "changeset:version": "changeset version",
46
- "ci:format": "oxfmt --check .",
47
- "ci:lint": "oxlint --deny-warnings .",
48
45
  "ci:test": "vitest run",
49
46
  "ci:typecheck": "tsc",
50
- "format": "oxfmt --write .",
51
- "lint": "oxlint .",
52
47
  "pack:check": "npm pack --dry-run",
53
- "release": "bun run build && changeset publish",
54
48
  "test": "vitest watch",
55
49
  "typecheck": "tsc"
56
50
  },
57
51
  "devDependencies": {
58
- "@arethetypeswrong/core": "0.18.5",
59
- "@changesets/changelog-github": "1.0.1",
60
- "@changesets/cli": "3.0.3",
61
- "@react-three/fiber": "9.7.0",
62
- "@types/react": "19.3.0",
63
- "@types/three": "0.186.0",
64
- "oxfmt": "0.68.0",
65
- "oxlint": "1.83.0",
66
- "publint": "0.3.24",
67
- "react": "19.3.0",
68
- "three": "0.186.0",
69
- "tsdown": "0.23.0",
70
- "typescript": "7.0.2",
71
- "vitest": "5.0.1"
52
+ "@arethetypeswrong/core": "catalog:",
53
+ "@react-three/fiber": "catalog:",
54
+ "@types/react": "catalog:",
55
+ "@types/three": "catalog:",
56
+ "publint": "catalog:",
57
+ "react": "catalog:",
58
+ "three": "catalog:",
59
+ "tsdown": "catalog:",
60
+ "typescript": "catalog:",
61
+ "vitest": "catalog:"
72
62
  },
73
63
  "peerDependencies": {
74
64
  "@react-three/fiber": ">=8",