gladly-plot 0.0.4 → 0.0.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 +9 -2
- package/package.json +10 -11
- package/src/axes/Axis.js +401 -0
- package/src/{AxisLink.js → axes/AxisLink.js} +6 -2
- package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
- package/src/axes/AxisRegistry.js +179 -0
- package/src/axes/Camera.js +47 -0
- package/src/axes/ColorAxisRegistry.js +101 -0
- package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
- package/src/axes/TickLabelAtlas.js +99 -0
- package/src/axes/ZoomController.js +463 -0
- package/src/colorscales/BivariateColorscales.js +205 -0
- package/src/colorscales/ColorscaleRegistry.js +144 -0
- package/src/compute/ComputationRegistry.js +179 -0
- package/src/compute/axisFilter.js +59 -0
- package/src/compute/conv.js +286 -0
- package/src/compute/elementwise.js +72 -0
- package/src/compute/fft.js +378 -0
- package/src/compute/filter.js +229 -0
- package/src/compute/hist.js +285 -0
- package/src/compute/kde.js +120 -0
- package/src/compute/scatter2dInterpolate.js +277 -0
- package/src/compute/util.js +196 -0
- package/src/core/ComputePipeline.js +153 -0
- package/src/core/GlBase.js +141 -0
- package/src/core/Layer.js +59 -0
- package/src/core/LayerType.js +433 -0
- package/src/core/Plot.js +1213 -0
- package/src/core/PlotGroup.js +204 -0
- package/src/core/ShaderQueue.js +73 -0
- package/src/data/ColumnData.js +269 -0
- package/src/data/Computation.js +95 -0
- package/src/data/Data.js +270 -0
- package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
- package/src/floats/Colorbar2d.js +77 -0
- package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
- package/src/{FilterbarFloat.js → floats/Float.js} +73 -30
- package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
- package/src/index.js +47 -22
- package/src/layers/BarsLayer.js +168 -0
- package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +12 -16
- package/src/layers/ColorbarLayer2d.js +86 -0
- package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +6 -5
- package/src/layers/LinesLayer.js +185 -0
- package/src/layers/PointsLayer.js +118 -0
- package/src/layers/ScatterShared.js +98 -0
- package/src/{TileLayer.js → layers/TileLayer.js} +24 -20
- package/src/math/mat4.js +100 -0
- package/src/Axis.js +0 -48
- package/src/AxisRegistry.js +0 -54
- package/src/ColorAxisRegistry.js +0 -49
- package/src/ColorscaleRegistry.js +0 -52
- package/src/Data.js +0 -67
- package/src/Float.js +0 -159
- package/src/Layer.js +0 -44
- package/src/LayerType.js +0 -209
- package/src/Plot.js +0 -1073
- package/src/ScatterLayer.js +0 -287
- /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
- /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AXES } from "../axes/AxisRegistry.js"
|
|
2
|
+
import { linkAxes } from "../axes/AxisLink.js"
|
|
3
|
+
import { normalizeData } from "../data/Data.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Coordinates a set of named Plot instances.
|
|
7
|
+
*
|
|
8
|
+
* - update({ data, plots }) normalizes data once so all plots share the same
|
|
9
|
+
* DataGroup, and updates all plots before re-establishing any auto-links,
|
|
10
|
+
* so intermediate QK mismatches never reach linkAxes().
|
|
11
|
+
*
|
|
12
|
+
* - When autoLink is true, all axes across all plots that share the same
|
|
13
|
+
* quantity kind are automatically linked. Axes that no longer share a QK
|
|
14
|
+
* after an update are simply unlinked rather than throwing.
|
|
15
|
+
*
|
|
16
|
+
* - When autoLink is false, manual links created via linkAxes() on axes
|
|
17
|
+
* belonging to plots in the group survive PlotGroup.update() calls
|
|
18
|
+
* unchanged (Axis instances are stable across plot updates).
|
|
19
|
+
*/
|
|
20
|
+
export class PlotGroup {
|
|
21
|
+
constructor(plots = {}, { autoLink = false } = {}) {
|
|
22
|
+
this._plots = new Map()
|
|
23
|
+
this._autoLink = autoLink
|
|
24
|
+
// key → { unlink, plotA, plotB }
|
|
25
|
+
this._links = new Map()
|
|
26
|
+
|
|
27
|
+
for (const [name, plot] of Object.entries(plots)) {
|
|
28
|
+
this._plots.set(name, plot)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (autoLink) this._updateAutoLinks()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Add a named plot to the group. Re-runs auto-linking if enabled. */
|
|
35
|
+
add(name, plot) {
|
|
36
|
+
this._plots.set(name, plot)
|
|
37
|
+
if (this._autoLink) this._updateAutoLinks()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Remove a named plot from the group, tearing down any links involving it. */
|
|
41
|
+
remove(name) {
|
|
42
|
+
if (!this._plots.has(name)) return
|
|
43
|
+
this._removeLinksForPlot(name)
|
|
44
|
+
this._plots.delete(name)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Update plots in the group.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} options
|
|
51
|
+
* @param {*} [options.data] - Raw data passed to all plots (normalized once).
|
|
52
|
+
* @param {object} [options.plots] - Map of { plotName: plotConfig } to update individually.
|
|
53
|
+
*/
|
|
54
|
+
async update({ data, plots } = {}) {
|
|
55
|
+
// Normalize data once so every plot receives the same DataGroup instance.
|
|
56
|
+
const normalizedData = data !== undefined ? normalizeData(data) : undefined
|
|
57
|
+
|
|
58
|
+
// Drop auto-links before updating any plot so intermediate states (one plot
|
|
59
|
+
// updated, the other not yet) don't trigger false QK mismatch errors.
|
|
60
|
+
if (this._autoLink) {
|
|
61
|
+
for (const entry of this._links.values()) entry.unlink()
|
|
62
|
+
this._links.clear()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Collect which plots will actually be updated, and snapshot their state
|
|
66
|
+
// so we can roll all of them back atomically if validation fails.
|
|
67
|
+
const toUpdate = []
|
|
68
|
+
for (const [name, plot] of this._plots) {
|
|
69
|
+
const plotConfig = plots?.[name]
|
|
70
|
+
if (normalizedData === undefined && plotConfig === undefined) continue
|
|
71
|
+
toUpdate.push({ name, plot, prevConfig: plot.currentConfig, prevRawData: plot._rawData })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Phase 1: apply all updates (no link validation yet).
|
|
76
|
+
for (const { name, plot } of toUpdate) {
|
|
77
|
+
const arg = {}
|
|
78
|
+
if (normalizedData !== undefined) arg.data = normalizedData
|
|
79
|
+
if (plots?.[name] !== undefined) arg.config = plots[name]
|
|
80
|
+
await plot._applyUpdate(arg)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Phase 2: validate links across every plot now that all QKs are final.
|
|
84
|
+
for (const [, plot] of this._plots) plot._validateLinks()
|
|
85
|
+
|
|
86
|
+
// Phase 3: reconcile auto-links with the new QKs in place.
|
|
87
|
+
if (this._autoLink) this._updateAutoLinks()
|
|
88
|
+
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Roll back every plot that was updated.
|
|
91
|
+
for (const { plot, prevConfig, prevRawData } of toUpdate) {
|
|
92
|
+
plot.currentConfig = prevConfig
|
|
93
|
+
plot._rawData = prevRawData
|
|
94
|
+
try { await plot._applyUpdate({}) } catch (e) {
|
|
95
|
+
console.error('[gladly] PlotGroup: error during rollback re-render:', e)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Restore auto-links to match the rolled-back state.
|
|
99
|
+
if (this._autoLink) this._updateAutoLinks()
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Tear down all auto-managed links. Does not destroy the plots themselves. */
|
|
105
|
+
destroy() {
|
|
106
|
+
for (const entry of this._links.values()) entry.unlink()
|
|
107
|
+
this._links.clear()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
_updateAutoLinks() {
|
|
113
|
+
// Collect all axes grouped by quantity kind across all plots.
|
|
114
|
+
// Spatial, color, and filter axes are all handled uniformly:
|
|
115
|
+
// plot._getAxis(id) returns a stable Axis instance for any id.
|
|
116
|
+
const qkAxes = new Map() // QK → [{ plotName, axisId }]
|
|
117
|
+
|
|
118
|
+
for (const [plotName, plot] of this._plots) {
|
|
119
|
+
// Spatial axes
|
|
120
|
+
if (plot.axisRegistry) {
|
|
121
|
+
for (const axisId of AXES) {
|
|
122
|
+
const qk = plot.getAxisQuantityKind(axisId)
|
|
123
|
+
if (!qk) continue
|
|
124
|
+
_push(qkAxes, qk, { plotName, axisId })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Color axes (axisId === quantityKind for non-spatial axes)
|
|
129
|
+
if (plot.colorAxisRegistry) {
|
|
130
|
+
for (const qk of plot.colorAxisRegistry.getQuantityKinds()) {
|
|
131
|
+
_push(qkAxes, qk, { plotName, axisId: qk })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Filter axes
|
|
136
|
+
if (plot.filterAxisRegistry) {
|
|
137
|
+
for (const qk of plot.filterAxisRegistry.getQuantityKinds()) {
|
|
138
|
+
_push(qkAxes, qk, { plotName, axisId: qk })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Determine which links should exist.
|
|
144
|
+
const desiredKeys = new Set()
|
|
145
|
+
for (const entries of qkAxes.values()) {
|
|
146
|
+
if (entries.length < 2) continue
|
|
147
|
+
for (let i = 0; i < entries.length; i++) {
|
|
148
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
149
|
+
desiredKeys.add(_linkKey(entries[i], entries[j]))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Remove stale links.
|
|
155
|
+
for (const [key, entry] of this._links) {
|
|
156
|
+
if (!desiredKeys.has(key)) {
|
|
157
|
+
entry.unlink()
|
|
158
|
+
this._links.delete(key)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create missing links.
|
|
163
|
+
for (const [, entries] of qkAxes) {
|
|
164
|
+
if (entries.length < 2) continue
|
|
165
|
+
for (let i = 0; i < entries.length; i++) {
|
|
166
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
167
|
+
const a = entries[i], b = entries[j]
|
|
168
|
+
const key = _linkKey(a, b)
|
|
169
|
+
if (this._links.has(key)) continue
|
|
170
|
+
|
|
171
|
+
const axisA = this._plots.get(a.plotName)._getAxis(a.axisId)
|
|
172
|
+
const axisB = this._plots.get(b.plotName)._getAxis(b.axisId)
|
|
173
|
+
// Both axes share the same QK (guaranteed by qkAxes grouping),
|
|
174
|
+
// so linkAxes() will not throw.
|
|
175
|
+
const handle = linkAxes(axisA, axisB)
|
|
176
|
+
this._links.set(key, { unlink: handle.unlink, plotA: a.plotName, plotB: b.plotName })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_removeLinksForPlot(name) {
|
|
183
|
+
for (const [key, entry] of this._links) {
|
|
184
|
+
if (entry.plotA === name || entry.plotB === name) {
|
|
185
|
+
entry.unlink()
|
|
186
|
+
this._links.delete(key)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function _push(map, key, value) {
|
|
195
|
+
if (!map.has(key)) map.set(key, [])
|
|
196
|
+
map.get(key).push(value)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Canonical link key — lexicographically sorted so (A,B) === (B,A). */
|
|
200
|
+
function _linkKey(a, b) {
|
|
201
|
+
const ka = `${a.plotName}\0${a.axisId}`
|
|
202
|
+
const kb = `${b.plotName}\0${b.axisId}`
|
|
203
|
+
return ka < kb ? `${ka}--${kb}` : `${kb}--${ka}`
|
|
204
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel shader compilation helpers.
|
|
3
|
+
*
|
|
4
|
+
* enqueueRegl(regl, config) — drop-in for regl(config) that defers compilation.
|
|
5
|
+
* Returns a callable handle backed by a null command until compileEnqueuedShaders()
|
|
6
|
+
* is called. State is stored on the regl object itself so no separate instance
|
|
7
|
+
* is needed.
|
|
8
|
+
*
|
|
9
|
+
* compileEnqueuedShaders(regl) — compiles all enqueued programs in parallel:
|
|
10
|
+
* 1. Kicks off raw GL compilation for every queued program without checking
|
|
11
|
+
* status, so the GPU driver can pipeline them concurrently.
|
|
12
|
+
* 2. Forces completion by checking LINK_STATUS on each (blocks only on
|
|
13
|
+
* stragglers; the rest are already done).
|
|
14
|
+
* 3. Discards the raw programs — they exist only to warm the driver's shader
|
|
15
|
+
* binary cache (e.g. ANGLE on Chrome/Edge, Mesa on Linux).
|
|
16
|
+
* 4. Creates real regl commands (driver returns cached binaries immediately)
|
|
17
|
+
* and resolves all handles.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export function enqueueRegl(regl, config) {
|
|
21
|
+
if (!regl._shaderQueue) regl._shaderQueue = []
|
|
22
|
+
|
|
23
|
+
let realCmd = null
|
|
24
|
+
const handle = (props) => realCmd(props)
|
|
25
|
+
handle._config = config
|
|
26
|
+
handle._resolve = (cmd) => { realCmd = cmd }
|
|
27
|
+
regl._shaderQueue.push(handle)
|
|
28
|
+
return handle
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function compileEnqueuedShaders(regl) {
|
|
32
|
+
const queue = regl._shaderQueue ?? []
|
|
33
|
+
regl._shaderQueue = null
|
|
34
|
+
|
|
35
|
+
if (queue.length === 0) return
|
|
36
|
+
|
|
37
|
+
const gl = regl._gl
|
|
38
|
+
|
|
39
|
+
// Phase 1: start all compilations without checking status
|
|
40
|
+
const precompiled = queue.map(({ _config: { vert, frag } }) => {
|
|
41
|
+
const vs = gl.createShader(gl.VERTEX_SHADER)
|
|
42
|
+
gl.shaderSource(vs, vert)
|
|
43
|
+
gl.compileShader(vs)
|
|
44
|
+
|
|
45
|
+
const fs = gl.createShader(gl.FRAGMENT_SHADER)
|
|
46
|
+
gl.shaderSource(fs, frag)
|
|
47
|
+
gl.compileShader(fs)
|
|
48
|
+
|
|
49
|
+
const prog = gl.createProgram()
|
|
50
|
+
gl.attachShader(prog, vs)
|
|
51
|
+
gl.attachShader(prog, fs)
|
|
52
|
+
gl.linkProgram(prog)
|
|
53
|
+
|
|
54
|
+
return { prog, vs, fs }
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Phase 2: wait for all (they've been compiling in parallel)
|
|
58
|
+
for (const { prog, vs, fs } of precompiled) {
|
|
59
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
60
|
+
console.warn('[gladly] Shader pre-compilation failed; regl will report the detailed error')
|
|
61
|
+
}
|
|
62
|
+
gl.detachShader(prog, vs)
|
|
63
|
+
gl.detachShader(prog, fs)
|
|
64
|
+
gl.deleteShader(vs)
|
|
65
|
+
gl.deleteShader(fs)
|
|
66
|
+
gl.deleteProgram(prog)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Phase 3: create real regl commands (driver binary cache hit)
|
|
70
|
+
for (const handle of queue) {
|
|
71
|
+
handle._resolve(regl(handle._config))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// ─── GLSL helper injected into any shader that samples column data ─────────────
|
|
2
|
+
// Values are packed 4 per texel (RGBA). Element i → texel i/4, channel i%4.
|
|
3
|
+
export const SAMPLE_COLUMN_GLSL = `float sampleColumn(sampler2D tex, float idx) {
|
|
4
|
+
ivec2 sz = textureSize(tex, 0);
|
|
5
|
+
int i = int(idx);
|
|
6
|
+
int texelI = i / 4;
|
|
7
|
+
int chan = i % 4;
|
|
8
|
+
ivec2 coord = ivec2(texelI % sz.x, texelI / sz.x);
|
|
9
|
+
vec4 texel = texelFetch(tex, coord, 0);
|
|
10
|
+
if (chan == 0) return texel.r;
|
|
11
|
+
if (chan == 1) return texel.g;
|
|
12
|
+
if (chan == 2) return texel.b;
|
|
13
|
+
return texel.a;
|
|
14
|
+
}`
|
|
15
|
+
|
|
16
|
+
// ─── GLSL helper for multi-dimensional column sampling ───────────────────────
|
|
17
|
+
// shape.xyzw holds the size of each logical dimension (unused dims = 1).
|
|
18
|
+
// idx is the multi-dimensional index, row-major (first dim varies fastest).
|
|
19
|
+
export const SAMPLE_COLUMN_ND_GLSL = `float sampleColumnND(sampler2D tex, ivec4 shape, ivec4 idx) {
|
|
20
|
+
int i = idx.x + shape.x * (idx.y + shape.y * (idx.z + shape.z * idx.w));
|
|
21
|
+
ivec2 sz = textureSize(tex, 0);
|
|
22
|
+
int texelI = i / 4;
|
|
23
|
+
int chan = i % 4;
|
|
24
|
+
ivec2 coord = ivec2(texelI % sz.x, texelI / sz.x);
|
|
25
|
+
vec4 texel = texelFetch(tex, coord, 0);
|
|
26
|
+
if (chan == 0) return texel.r;
|
|
27
|
+
if (chan == 1) return texel.g;
|
|
28
|
+
if (chan == 2) return texel.b;
|
|
29
|
+
return texel.a;
|
|
30
|
+
}`
|
|
31
|
+
|
|
32
|
+
// Upload a Float32Array as a 2D RGBA texture with 4 values packed per texel.
|
|
33
|
+
export function uploadToTexture(regl, array) {
|
|
34
|
+
const nTexels = Math.ceil(array.length / 4)
|
|
35
|
+
const w = Math.min(nTexels, regl.limits.maxTextureSize)
|
|
36
|
+
const h = Math.ceil(nTexels / w)
|
|
37
|
+
const texData = new Float32Array(w * h * 4)
|
|
38
|
+
for (let i = 0; i < array.length; i++) texData[i] = array[i]
|
|
39
|
+
const tex = regl.texture({ data: texData, shape: [w, h], type: 'float', format: 'rgba' })
|
|
40
|
+
tex._dataLength = array.length
|
|
41
|
+
return tex
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── ColumnData base class ────────────────────────────────────────────────────
|
|
45
|
+
export class ColumnData {
|
|
46
|
+
get length() { return null }
|
|
47
|
+
get domain() { return null }
|
|
48
|
+
get quantityKind() { return null }
|
|
49
|
+
get shape() { return [this.length] }
|
|
50
|
+
get ndim() { return this.shape.length }
|
|
51
|
+
get totalLength() { return this.shape.reduce((a, b) => a * b, 1) }
|
|
52
|
+
|
|
53
|
+
// Returns { glslExpr: string, textures: { uniformName: () => reglTexture } }
|
|
54
|
+
// path must be a valid GLSL identifier fragment (no dots or special chars)
|
|
55
|
+
resolve(path, regl) { throw new Error('Not implemented') }
|
|
56
|
+
|
|
57
|
+
// Returns a regl texture (4 values per texel, RGBA, 2D layout).
|
|
58
|
+
// May run a GPU render pass for GlslColumn.
|
|
59
|
+
toTexture(regl) { throw new Error('Not implemented') }
|
|
60
|
+
|
|
61
|
+
// Called before each render to refresh axis-dependent textures.
|
|
62
|
+
// Returns true if the texture was updated.
|
|
63
|
+
refresh(plot) { return false }
|
|
64
|
+
|
|
65
|
+
// Returns a new ColumnData that samples at a_pickId + (offsetExpr) instead of a_pickId.
|
|
66
|
+
// offsetExpr is a GLSL expression string, e.g. 'a_endPoint' or '1.0'.
|
|
67
|
+
withOffset(offsetExpr) { return new OffsetColumn(this, offsetExpr) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── ArrayColumn ──────────────────────────────────────────────────────────────
|
|
71
|
+
export class ArrayColumn extends ColumnData {
|
|
72
|
+
constructor(array, { domain = null, quantityKind = null, shape = null } = {}) {
|
|
73
|
+
super()
|
|
74
|
+
this._array = array
|
|
75
|
+
this._domain = domain
|
|
76
|
+
this._quantityKind = quantityKind
|
|
77
|
+
this._shape = shape
|
|
78
|
+
this._ref = null // { texture } lazy
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get length() { return this._array.length }
|
|
82
|
+
get domain() { return this._domain }
|
|
83
|
+
get quantityKind() { return this._quantityKind }
|
|
84
|
+
get array() { return this._array }
|
|
85
|
+
get shape() { return this._shape ?? [this._array.length] }
|
|
86
|
+
|
|
87
|
+
_upload(regl) {
|
|
88
|
+
if (this._array.length === 0) {
|
|
89
|
+
throw new Error(`[gladly] ArrayColumn: cannot upload empty array as texture — the data source has 0 elements`)
|
|
90
|
+
}
|
|
91
|
+
if (!this._ref) this._ref = { texture: uploadToTexture(regl, this._array) }
|
|
92
|
+
return this._ref
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resolve(path, regl) {
|
|
96
|
+
const ref = this._upload(regl)
|
|
97
|
+
const uName = `u_col_${path}`
|
|
98
|
+
const shape = this.shape
|
|
99
|
+
if (shape.length === 1) {
|
|
100
|
+
return { glslExpr: `sampleColumn(${uName}, a_pickId)`, textures: { [uName]: () => ref.texture }, shape }
|
|
101
|
+
}
|
|
102
|
+
return { glslExpr: null, textures: { [uName]: () => ref.texture }, shape }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
toTexture(regl) { return this._upload(regl).texture }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── TextureColumn ────────────────────────────────────────────────────────────
|
|
109
|
+
export class TextureColumn extends ColumnData {
|
|
110
|
+
constructor(ref, { domain = null, quantityKind = null, length = null, refreshFn = null, shape = null } = {}) {
|
|
111
|
+
super()
|
|
112
|
+
this._ref = ref // { texture } mutable so hot-swaps propagate
|
|
113
|
+
this._domain = domain
|
|
114
|
+
this._quantityKind = quantityKind
|
|
115
|
+
this._length = length
|
|
116
|
+
this._refreshFn = refreshFn
|
|
117
|
+
this._shape = shape
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get length() { return this._length }
|
|
121
|
+
get domain() { return this._domain }
|
|
122
|
+
get quantityKind() { return this._quantityKind }
|
|
123
|
+
get shape() { return this._shape ?? (this._length != null ? [this._length] : [0]) }
|
|
124
|
+
|
|
125
|
+
resolve(path, regl) {
|
|
126
|
+
if (!this._ref.texture) {
|
|
127
|
+
throw new Error(`[gladly] TextureColumn '${path}': texture is null — the column was not properly initialized or its computation failed`)
|
|
128
|
+
}
|
|
129
|
+
const uName = `u_col_${path}`
|
|
130
|
+
const texFn = () => {
|
|
131
|
+
if (!this._ref.texture) throw new Error(`[gladly] TextureColumn '${path}': texture became null after initialization`)
|
|
132
|
+
return this._ref.texture
|
|
133
|
+
}
|
|
134
|
+
const shape = this.shape
|
|
135
|
+
if (shape.length === 1) {
|
|
136
|
+
return { glslExpr: `sampleColumn(${uName}, a_pickId)`, textures: { [uName]: texFn }, shape }
|
|
137
|
+
}
|
|
138
|
+
return { glslExpr: null, textures: { [uName]: texFn }, shape }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
toTexture(regl) {
|
|
142
|
+
if (!this._ref.texture) {
|
|
143
|
+
throw new Error(`[gladly] TextureColumn.toTexture(): texture is null — the column was not properly initialized`)
|
|
144
|
+
}
|
|
145
|
+
return this._ref.texture
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async refresh(plot) {
|
|
149
|
+
if (this._refreshFn) return await this._refreshFn(plot, this._ref) ?? false
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── GlslColumn ───────────────────────────────────────────────────────────────
|
|
155
|
+
export class GlslColumn extends ColumnData {
|
|
156
|
+
constructor(inputs, glslFn, { domain = null, quantityKind = null, shape = null } = {}) {
|
|
157
|
+
super()
|
|
158
|
+
this._inputs = inputs // { name: ColumnData }
|
|
159
|
+
this._glslFn = glslFn // (resolvedExprs: { name: string }) => string
|
|
160
|
+
this._domain = domain
|
|
161
|
+
this._quantityKind = quantityKind
|
|
162
|
+
this._targetShape = shape // logical output shape (null = 1D, infer from inputs)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get length() {
|
|
166
|
+
if (this._targetShape) return this._targetShape.reduce((a, b) => a * b, 1)
|
|
167
|
+
return Object.values(this._inputs)[0]?.length ?? null
|
|
168
|
+
}
|
|
169
|
+
get domain() { return this._domain }
|
|
170
|
+
get quantityKind() { return this._quantityKind }
|
|
171
|
+
get shape() {
|
|
172
|
+
if (this._targetShape) return this._targetShape
|
|
173
|
+
const l = Object.values(this._inputs)[0]?.length ?? null
|
|
174
|
+
return l != null ? [l] : [0]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
resolve(path, regl) {
|
|
178
|
+
const resolvedExprs = {}
|
|
179
|
+
const textures = {}
|
|
180
|
+
for (const [name, col] of Object.entries(this._inputs)) {
|
|
181
|
+
const { glslExpr, textures: colTextures } = col.resolve(`${path}_${name}`, regl)
|
|
182
|
+
resolvedExprs[name] = glslExpr
|
|
183
|
+
Object.assign(textures, colTextures)
|
|
184
|
+
}
|
|
185
|
+
return { glslExpr: this._glslFn(resolvedExprs), textures }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
toTexture(regl) {
|
|
189
|
+
const N = this.length
|
|
190
|
+
if (N === null) throw new Error('GlslColumn: cannot determine length for toTexture()')
|
|
191
|
+
const nTexels = Math.ceil(N / 4)
|
|
192
|
+
const w = Math.min(nTexels, regl.limits.maxTextureSize)
|
|
193
|
+
const h = Math.ceil(nTexels / w)
|
|
194
|
+
const { glslExpr, textures } = this.resolve('glsl_mat', regl)
|
|
195
|
+
const samplerDecls = Object.keys(textures).map(n => `uniform sampler2D ${n};`).join('\n')
|
|
196
|
+
const vert = `#version 300 es
|
|
197
|
+
precision highp float;
|
|
198
|
+
in vec2 a_position;
|
|
199
|
+
void main() {
|
|
200
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
201
|
+
}`
|
|
202
|
+
const frag = `#version 300 es
|
|
203
|
+
precision highp float;
|
|
204
|
+
precision highp sampler2D;
|
|
205
|
+
${samplerDecls}
|
|
206
|
+
${SAMPLE_COLUMN_GLSL}
|
|
207
|
+
out vec4 fragColor;
|
|
208
|
+
float gladly_eval(float a_pickId) {
|
|
209
|
+
return ${glslExpr};
|
|
210
|
+
}
|
|
211
|
+
void main() {
|
|
212
|
+
int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
|
|
213
|
+
int base = texelI * 4;
|
|
214
|
+
float v0 = base + 0 < ${N} ? gladly_eval(float(base + 0)) : 0.0;
|
|
215
|
+
float v1 = base + 1 < ${N} ? gladly_eval(float(base + 1)) : 0.0;
|
|
216
|
+
float v2 = base + 2 < ${N} ? gladly_eval(float(base + 2)) : 0.0;
|
|
217
|
+
float v3 = base + 3 < ${N} ? gladly_eval(float(base + 3)) : 0.0;
|
|
218
|
+
fragColor = vec4(v0, v1, v2, v3);
|
|
219
|
+
}`
|
|
220
|
+
const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
|
|
221
|
+
const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
|
|
222
|
+
const uniforms = {}
|
|
223
|
+
for (const [k, fn] of Object.entries(textures)) uniforms[k] = fn
|
|
224
|
+
regl({
|
|
225
|
+
framebuffer: outputFBO, vert, frag,
|
|
226
|
+
attributes: { a_position: [[-1, -1], [1, -1], [-1, 1], [1, 1]] },
|
|
227
|
+
uniforms,
|
|
228
|
+
count: 4,
|
|
229
|
+
primitive: 'triangle strip'
|
|
230
|
+
})()
|
|
231
|
+
outputTex._dataLength = N
|
|
232
|
+
return outputTex
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
refresh(plot) {
|
|
236
|
+
let changed = false
|
|
237
|
+
for (const col of Object.values(this._inputs)) {
|
|
238
|
+
if (col.refresh(plot)) changed = true
|
|
239
|
+
}
|
|
240
|
+
return changed
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── OffsetColumn ─────────────────────────────────────────────────────────────
|
|
245
|
+
// Wraps a ColumnData and shifts the GLSL sampling index by a GLSL expression.
|
|
246
|
+
// Produced by col.withOffset(offsetExpr).
|
|
247
|
+
export class OffsetColumn extends ColumnData {
|
|
248
|
+
constructor(base, offsetExpr) {
|
|
249
|
+
super()
|
|
250
|
+
this._base = base
|
|
251
|
+
this._offsetExpr = offsetExpr
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
get length() { return this._base.length }
|
|
255
|
+
get domain() { return this._base.domain }
|
|
256
|
+
get quantityKind() { return this._base.quantityKind }
|
|
257
|
+
get shape() { return this._base.shape }
|
|
258
|
+
|
|
259
|
+
resolve(path, regl) {
|
|
260
|
+
const { glslExpr, textures } = this._base.resolve(path, regl)
|
|
261
|
+
return {
|
|
262
|
+
glslExpr: glslExpr.replace('a_pickId', `(a_pickId + (${this._offsetExpr}))`),
|
|
263
|
+
textures
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
toTexture(regl) { return this._base.toTexture(regl) }
|
|
268
|
+
refresh(plot) { return this._base.refresh(plot) }
|
|
269
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ColumnData, TextureColumn, GlslColumn } from './ColumnData.js'
|
|
2
|
+
|
|
3
|
+
function domainsEqual(a, b) {
|
|
4
|
+
if (a === b) return true
|
|
5
|
+
if (a == null || b == null) return a === b
|
|
6
|
+
return a[0] === b[0] && a[1] === b[1]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ─── Base classes ─────────────────────────────────────────────────────────────
|
|
10
|
+
export class Computation {
|
|
11
|
+
schema(data) { throw new Error('Not implemented') }
|
|
12
|
+
getQuantityKind(params, data) { return null }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ComputedData {
|
|
16
|
+
columns() { throw new Error('Not implemented') }
|
|
17
|
+
compute(regl, params, data, getAxisDomain) { throw new Error('Not implemented') }
|
|
18
|
+
schema(data) { throw new Error('Not implemented') }
|
|
19
|
+
filterAxes(params, data) { return {} }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class TextureComputation extends Computation {
|
|
23
|
+
// Override: inputs is { name: ColumnData | scalar }, returns raw regl texture.
|
|
24
|
+
compute(regl, inputs, getAxisDomain) { throw new Error('Not implemented') }
|
|
25
|
+
|
|
26
|
+
// Override to declare output shape in JS without running GPU work.
|
|
27
|
+
// Return int[] (e.g. [W, H] for a 2D output), or null to fall back to 1D (_dataLength).
|
|
28
|
+
// inputs values are ColumnData (shapes accessible) or scalars.
|
|
29
|
+
outputShape(inputs) { return null }
|
|
30
|
+
|
|
31
|
+
async createColumn(regl, inputs, plot) {
|
|
32
|
+
const accessedAxes = new Set()
|
|
33
|
+
const cachedDomains = {}
|
|
34
|
+
|
|
35
|
+
const getAxisDomain = (axisId) => {
|
|
36
|
+
accessedAxes.add(axisId)
|
|
37
|
+
return plot ? plot.getAxisDomain(axisId) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rawTex = await this.compute(regl, inputs, getAxisDomain)
|
|
41
|
+
const ref = { texture: rawTex }
|
|
42
|
+
|
|
43
|
+
const hasColumnInputs = Object.values(inputs).some(v => v instanceof ColumnData)
|
|
44
|
+
let refreshFn = null
|
|
45
|
+
if (accessedAxes.size > 0 || hasColumnInputs) {
|
|
46
|
+
for (const axisId of accessedAxes) {
|
|
47
|
+
cachedDomains[axisId] = plot ? plot.getAxisDomain(axisId) : null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const comp = this
|
|
51
|
+
refreshFn = async (currentPlot, texRef) => {
|
|
52
|
+
// Refresh inputs first; track if any updated
|
|
53
|
+
let inputsRefreshed = false
|
|
54
|
+
for (const val of Object.values(inputs)) {
|
|
55
|
+
if (val instanceof ColumnData && await val.refresh(currentPlot)) inputsRefreshed = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let ownAxisChanged = false
|
|
59
|
+
for (const axisId of accessedAxes) {
|
|
60
|
+
if (!domainsEqual(currentPlot.getAxisDomain(axisId), cachedDomains[axisId])) {
|
|
61
|
+
ownAxisChanged = true
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!inputsRefreshed && !ownAxisChanged) return false
|
|
67
|
+
|
|
68
|
+
const newAxes = new Set()
|
|
69
|
+
const newGetter = (axisId) => { newAxes.add(axisId); return currentPlot.getAxisDomain(axisId) }
|
|
70
|
+
texRef.texture = await comp.compute(regl, inputs, newGetter)
|
|
71
|
+
|
|
72
|
+
accessedAxes.clear()
|
|
73
|
+
for (const axisId of newAxes) {
|
|
74
|
+
accessedAxes.add(axisId)
|
|
75
|
+
cachedDomains[axisId] = currentPlot.getAxisDomain(axisId)
|
|
76
|
+
}
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new TextureColumn(ref, {
|
|
82
|
+
length: rawTex._dataLength ?? rawTex.width,
|
|
83
|
+
shape: rawTex._dataShape ?? this.outputShape(inputs) ?? null,
|
|
84
|
+
refreshFn
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class GlslComputation extends Computation {
|
|
90
|
+
glsl(resolvedExprs) { throw new Error('Not implemented') }
|
|
91
|
+
|
|
92
|
+
createColumn(inputs, meta = {}) {
|
|
93
|
+
return new GlslColumn(inputs, resolvedExprs => this.glsl(resolvedExprs), meta)
|
|
94
|
+
}
|
|
95
|
+
}
|