// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import type { WebGLContextManager } from "../../../webgl/context-manager"; import type { CartesianChart } from "../cartesian"; import type { Glyph } from "../glyph"; import { bindGradientTexture } from "../../../webgl/gradient-texture"; import { compileProgram } from "../../../webgl/program-cache"; import { buildPointRowTooltipLines } from "../tooltip-lines"; import scatterVert from "../../../shaders/scatter.vert.glsl"; import scatterFrag from "../../../shaders/scatter.frag.glsl"; type GL = WebGL2RenderingContext | WebGLRenderingContext; interface PointCache { program: WebGLProgram; u_projection: WebGLUniformLocation | null; u_point_size: WebGLUniformLocation | null; u_color_range: WebGLUniformLocation | null; u_gradient_lut: WebGLUniformLocation | null; u_size_range: WebGLUniformLocation | null; u_point_size_range: WebGLUniformLocation | null; a_position: number; a_color_value: number; a_size_value: number; } /** * `gl.POINTS` glyph — one squared/antialiased point per data row. Color * and size are driven by the shared `a_color_value` / `a_size_value` * buffers; the vertex shader does sign-aware color-t mapping and samples * the gradient LUT. One draw call per series (the slot layout leaves * gaps at each series' tail that we can't safely include in a single * draw — dispatching `count[s]` per series skips them). */ export class PointGlyph implements Glyph { readonly name = "point" as const; private _cache: PointCache | null = null; ensureProgram( _chart: CartesianChart, glManager: WebGLContextManager, ): void { if (this._cache) { return; } this._cache = compileProgram( glManager, "scatter", scatterVert, scatterFrag, [ "u_projection", "u_point_size", "u_color_range", "u_gradient_lut", "u_size_range", "u_point_size_range", ], ["a_position", "a_color_value", "a_size_value"], ); } draw( chart: CartesianChart, glManager: WebGLContextManager, projection: Float32Array, ): void { const cache = this._cache; if (!cache) { return; } if (!bindPointState(cache, chart, glManager, projection)) { return; } // Per-series tight draws: each series `s` occupies slots // `[s*cap, s*cap + count[s])`. Dispatching `count[s]` avoids // rasterizing unused tail slots. All attribs have divisor=0 so // `first` shifts them together. const gl = glManager.gl; const numSeries = Math.max(1, chart._splitGroups.length); const cap = chart._seriesCapacity; for (let s = 0; s < numSeries; s++) { const count = chart._seriesUploadedCounts[s] ?? 0; if (count <= 0) { continue; } gl.drawArrays(gl.POINTS, s * cap, count); } } drawSeries( chart: CartesianChart, glManager: WebGLContextManager, projection: Float32Array, seriesIdx: number, ): void { const cache = this._cache; if (!cache) { return; } if (!bindPointState(cache, chart, glManager, projection)) { return; } const count = chart._seriesUploadedCounts[seriesIdx] ?? 0; if (count <= 0) { return; } const gl = glManager.gl; const cap = chart._seriesCapacity; gl.drawArrays(gl.POINTS, seriesIdx * cap, count); } buildTooltipLines( chart: CartesianChart, flatIdx: number, ): Promise { return buildPointRowTooltipLines(chart, flatIdx); } tooltipOptions() { return { crosshair: true, highlightRadius: 6 }; } destroy(_chart: CartesianChart): void { // Program lifetime is owned by the shader registry; just drop // the cache reference. No private GPU resources to free. this._cache = null; } } function setUniforms( cache: PointCache, gl: GL, projection: Float32Array, chart: CartesianChart, dpr: number, ): void { gl.uniformMatrix4fv(cache.u_projection, false, projection); gl.uniform1f(cache.u_point_size, chart._pluginConfig.point_size_px * dpr); if (chart._colorMin < chart._colorMax) { gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax); } else { gl.uniform2f(cache.u_color_range, 0.0, 0.0); } if (chart._sizeMin < chart._sizeMax) { gl.uniform2f(cache.u_size_range, chart._sizeMin, chart._sizeMax); } else { gl.uniform2f(cache.u_size_range, 0.0, 0.0); } const size_scale_factor = Math.min(chart._pluginConfig.point_size_px, 3); gl.uniform2f( cache.u_point_size_range, Math.max( 2 * dpr, (chart._pluginConfig.point_size_px / size_scale_factor) * dpr, ), chart._pluginConfig.point_size_px * size_scale_factor * dpr, ); } /** * Shared pre-draw state setup for `draw` and `drawSeries`. Binds the * program, uploads uniforms + gradient texture, wires the three per- * vertex attributes. Returns false if the gradient cache is missing. */ function bindPointState( cache: PointCache, chart: CartesianChart, glManager: WebGLContextManager, projection: Float32Array, ): boolean { const gl = glManager.gl; if (!chart._gradientCache) { return false; } gl.useProgram(cache.program); setUniforms(cache, gl, projection, chart, glManager.dpr); bindGradientTexture( glManager, chart._gradientCache.texture, cache.u_gradient_lut, 0, ); // Render-path uses `peek` (not `getOrCreate`) so we never // recreate buffers from the draw path. If a buffer hasn't been // uploaded yet — e.g. pan/zoom render landing between a pending // draw's `ensureBufferCapacity` and its `uploadChunk` — return // false and let the caller skip `drawArrays`. Painting against // a freshly-recreated zero-filled buffer would show one frame // of empty plot area while gridlines/chrome remain correct. const posBuf = glManager.bufferPool.peek("a_position"); const colorBuf = glManager.bufferPool.peek("a_color_value"); const sizeBuf = glManager.bufferPool.peek("a_size_value"); if (!posBuf || !colorBuf || !sizeBuf) { return false; } gl.bindBuffer(gl.ARRAY_BUFFER, posBuf.buffer); gl.enableVertexAttribArray(cache.a_position); gl.vertexAttribPointer(cache.a_position, 2, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf.buffer); gl.enableVertexAttribArray(cache.a_color_value); gl.vertexAttribPointer(cache.a_color_value, 1, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuf.buffer); gl.enableVertexAttribArray(cache.a_size_value); gl.vertexAttribPointer(cache.a_size_value, 1, gl.FLOAT, false, 0, 0); return true; }