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,196 @@
|
|
|
1
|
+
import { registerTextureComputation, registerGlslComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
|
|
2
|
+
import { TextureComputation, GlslComputation } from "../data/Computation.js"
|
|
3
|
+
import { GlslColumn } from "../data/ColumnData.js"
|
|
4
|
+
|
|
5
|
+
// Shared helper: allocate an output texture + FBO and run a fullscreen quad.
|
|
6
|
+
function runFullscreenQuad(regl, N, fragGlsl, uniforms = {}) {
|
|
7
|
+
const nTexels = Math.ceil(N / 4)
|
|
8
|
+
const w = Math.min(nTexels, regl.limits.maxTextureSize)
|
|
9
|
+
const h = Math.ceil(nTexels / w)
|
|
10
|
+
const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
|
|
11
|
+
const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
|
|
12
|
+
regl({
|
|
13
|
+
framebuffer: outputFBO,
|
|
14
|
+
vert: `#version 300 es
|
|
15
|
+
precision highp float;
|
|
16
|
+
in vec2 a_position;
|
|
17
|
+
void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`,
|
|
18
|
+
frag: fragGlsl(w),
|
|
19
|
+
attributes: { a_position: [[-1, -1], [1, -1], [-1, 1], [1, 1]] },
|
|
20
|
+
uniforms,
|
|
21
|
+
count: 4,
|
|
22
|
+
primitive: 'triangle strip'
|
|
23
|
+
})()
|
|
24
|
+
outputTex._dataLength = N
|
|
25
|
+
return outputTex
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── linspace ─────────────────────────────────────────────────────────────────
|
|
29
|
+
// Produces N values in ]0, 1[ : value[i] = (i + 0.5) / N — fully on GPU.
|
|
30
|
+
class LinspaceComputation extends TextureComputation {
|
|
31
|
+
compute(regl, inputs, _getAxisDomain) {
|
|
32
|
+
const N = inputs.length
|
|
33
|
+
return runFullscreenQuad(regl, N, w => `#version 300 es
|
|
34
|
+
precision highp float;
|
|
35
|
+
uniform int u_N;
|
|
36
|
+
out vec4 fragColor;
|
|
37
|
+
float linVal(int idx) { return (float(idx) + 0.5) / float(u_N); }
|
|
38
|
+
void main() {
|
|
39
|
+
int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
|
|
40
|
+
int base = texelI * 4;
|
|
41
|
+
fragColor = vec4(
|
|
42
|
+
base + 0 < u_N ? linVal(base + 0) : 0.0,
|
|
43
|
+
base + 1 < u_N ? linVal(base + 1) : 0.0,
|
|
44
|
+
base + 2 < u_N ? linVal(base + 2) : 0.0,
|
|
45
|
+
base + 3 < u_N ? linVal(base + 3) : 0.0
|
|
46
|
+
);
|
|
47
|
+
}`, { u_N: N })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
schema(_data) {
|
|
51
|
+
return {
|
|
52
|
+
type: 'object',
|
|
53
|
+
title: 'linspace',
|
|
54
|
+
properties: {
|
|
55
|
+
length: { type: 'integer', description: 'Number of values' }
|
|
56
|
+
},
|
|
57
|
+
required: ['length']
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
registerTextureComputation('linspace', new LinspaceComputation())
|
|
63
|
+
|
|
64
|
+
// ─── range ────────────────────────────────────────────────────────────────────
|
|
65
|
+
// Produces N integer values: 0, 1, 2, ..., N-1 — fully on GPU.
|
|
66
|
+
class RangeComputation extends TextureComputation {
|
|
67
|
+
compute(regl, inputs, _getAxisDomain) {
|
|
68
|
+
const N = inputs.length
|
|
69
|
+
return runFullscreenQuad(regl, N, w => `#version 300 es
|
|
70
|
+
precision highp float;
|
|
71
|
+
uniform int u_N;
|
|
72
|
+
out vec4 fragColor;
|
|
73
|
+
void main() {
|
|
74
|
+
int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
|
|
75
|
+
int base = texelI * 4;
|
|
76
|
+
fragColor = vec4(
|
|
77
|
+
base + 0 < u_N ? float(base + 0) : 0.0,
|
|
78
|
+
base + 1 < u_N ? float(base + 1) : 0.0,
|
|
79
|
+
base + 2 < u_N ? float(base + 2) : 0.0,
|
|
80
|
+
base + 3 < u_N ? float(base + 3) : 0.0
|
|
81
|
+
);
|
|
82
|
+
}`, { u_N: N })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
schema(_data) {
|
|
86
|
+
return {
|
|
87
|
+
type: 'object',
|
|
88
|
+
title: 'range',
|
|
89
|
+
properties: {
|
|
90
|
+
length: { type: 'integer', description: 'Number of values' }
|
|
91
|
+
},
|
|
92
|
+
required: ['length']
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
registerTextureComputation('range', new RangeComputation())
|
|
98
|
+
|
|
99
|
+
// ─── glslExpr ─────────────────────────────────────────────────────────────────
|
|
100
|
+
// A GlslComputation where the GLSL expression is a user-supplied string.
|
|
101
|
+
// Named inputs are referenced in expr as {name} placeholders.
|
|
102
|
+
//
|
|
103
|
+
// Example:
|
|
104
|
+
// { glslExpr: { expr: "sin({x}) * {y}", inputs: { x: "col1", y: "col2" } } }
|
|
105
|
+
class GlslExprComputation extends GlslComputation {
|
|
106
|
+
glsl(_resolvedExprs) { throw new Error('glslExpr: use createColumn, not glsl()') }
|
|
107
|
+
|
|
108
|
+
createColumn(inputs) {
|
|
109
|
+
const expr = inputs.expr // raw string from user
|
|
110
|
+
const colInputs = inputs.inputs ?? {}
|
|
111
|
+
return new GlslColumn(colInputs, (resolvedExprs) => {
|
|
112
|
+
let result = expr
|
|
113
|
+
for (const [name, glslExpr] of Object.entries(resolvedExprs)) {
|
|
114
|
+
result = result.replaceAll(`{${name}}`, glslExpr)
|
|
115
|
+
}
|
|
116
|
+
return result
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
schema(data) {
|
|
121
|
+
return {
|
|
122
|
+
type: 'object',
|
|
123
|
+
title: 'glslExpr',
|
|
124
|
+
properties: {
|
|
125
|
+
expr: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'GLSL expression; reference inputs as {name} placeholders'
|
|
128
|
+
},
|
|
129
|
+
inputs: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
additionalProperties: EXPRESSION_REF,
|
|
132
|
+
description: 'Named input columns referenced in expr as {name}'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
required: ['expr']
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
registerGlslComputation('glslExpr', new GlslExprComputation())
|
|
141
|
+
|
|
142
|
+
// ─── random ───────────────────────────────────────────────────────────────────
|
|
143
|
+
// Produces N pseudorandom values in ]0, 1[ derived from index ^ seed.
|
|
144
|
+
// All computation is done on the GPU via a fullscreen quad render pass.
|
|
145
|
+
// Hash: 3-round xorshift-multiply (good avalanche, no trig, GLSL ES 300).
|
|
146
|
+
class RandomComputation extends TextureComputation {
|
|
147
|
+
compute(regl, inputs, _getAxisDomain) {
|
|
148
|
+
const N = inputs.length
|
|
149
|
+
const seed = (inputs.seed || 0) === 0 ? (Math.random() * 0x7fffffff) | 0 : inputs.seed
|
|
150
|
+
return runFullscreenQuad(regl, N, w => `#version 300 es
|
|
151
|
+
precision highp float;
|
|
152
|
+
uniform int u_seed;
|
|
153
|
+
uniform int u_N;
|
|
154
|
+
out vec4 fragColor;
|
|
155
|
+
|
|
156
|
+
uint uhash(uint x) {
|
|
157
|
+
x ^= x >> 17u;
|
|
158
|
+
x *= 0xbf324c81u;
|
|
159
|
+
x ^= x >> 11u;
|
|
160
|
+
x *= 0x68bc4b39u;
|
|
161
|
+
x ^= x >> 16u;
|
|
162
|
+
return x;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Maps uint to ]0, 1[ : (bits + 0.5) / 2^24
|
|
166
|
+
float randVal(int idx) {
|
|
167
|
+
uint h = uhash(uint(idx) ^ uint(u_seed));
|
|
168
|
+
return (float(h >> 8u) + 0.5) / 16777216.0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void main() {
|
|
172
|
+
int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
|
|
173
|
+
int base = texelI * 4;
|
|
174
|
+
fragColor = vec4(
|
|
175
|
+
base + 0 < u_N ? randVal(base + 0) : 0.0,
|
|
176
|
+
base + 1 < u_N ? randVal(base + 1) : 0.0,
|
|
177
|
+
base + 2 < u_N ? randVal(base + 2) : 0.0,
|
|
178
|
+
base + 3 < u_N ? randVal(base + 3) : 0.0
|
|
179
|
+
);
|
|
180
|
+
}`, { u_seed: seed | 0, u_N: N })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
schema(_data) {
|
|
184
|
+
return {
|
|
185
|
+
type: 'object',
|
|
186
|
+
title: 'random',
|
|
187
|
+
properties: {
|
|
188
|
+
length: { type: 'integer', description: 'Number of values' },
|
|
189
|
+
seed: { type: 'integer', description: 'Integer seed (default 0)', default: 0 }
|
|
190
|
+
},
|
|
191
|
+
required: ['length']
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
registerTextureComputation('random', new RandomComputation())
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { GlBase } from "./GlBase.js"
|
|
2
|
+
import { FilterAxisRegistry } from "../axes/FilterAxisRegistry.js"
|
|
3
|
+
import { DataGroup, normalizeData } from "../data/Data.js"
|
|
4
|
+
import { ColumnData, ArrayColumn } from "../data/ColumnData.js"
|
|
5
|
+
|
|
6
|
+
// Read a 4-packed RGBA float texture back to a flat Float32Array of length dataLength.
|
|
7
|
+
function readTextureToArray(regl, texture) {
|
|
8
|
+
const dataLength = texture._dataLength ?? (texture.width * texture.height * 4)
|
|
9
|
+
const fbo = regl.framebuffer({ color: texture, depth: false })
|
|
10
|
+
let pixels
|
|
11
|
+
try {
|
|
12
|
+
regl({ framebuffer: fbo })(() => {
|
|
13
|
+
pixels = regl.read()
|
|
14
|
+
})
|
|
15
|
+
} finally {
|
|
16
|
+
fbo.destroy()
|
|
17
|
+
}
|
|
18
|
+
const arr = pixels instanceof Float32Array ? pixels : new Float32Array(pixels.buffer, pixels.byteOffset, pixels.byteLength / 4)
|
|
19
|
+
return arr.slice(0, dataLength)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Wraps any ColumnData and adds getArray() for CPU readback.
|
|
23
|
+
class ReadableColumn extends ColumnData {
|
|
24
|
+
constructor(col, regl) {
|
|
25
|
+
super()
|
|
26
|
+
this._col = col
|
|
27
|
+
this._regl = regl
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get length() { return this._col.length }
|
|
31
|
+
get domain() { return this._col.domain }
|
|
32
|
+
get quantityKind() { return this._col.quantityKind }
|
|
33
|
+
|
|
34
|
+
resolve(path, regl) { return this._col.resolve(path, regl) }
|
|
35
|
+
toTexture(regl) { return this._col.toTexture(regl) }
|
|
36
|
+
refresh(plot) { return this._col.refresh(plot) }
|
|
37
|
+
withOffset(expr) { return this._col.withOffset(expr) }
|
|
38
|
+
|
|
39
|
+
getArray() {
|
|
40
|
+
if (this._col instanceof ArrayColumn) return this._col.array
|
|
41
|
+
const tex = this._col.toTexture(this._regl)
|
|
42
|
+
return readTextureToArray(this._regl, tex)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Output object returned by ComputePipeline.update().
|
|
47
|
+
// Like DataGroup but getData() returns ReadableColumn with getArray(),
|
|
48
|
+
// and getArrays() reads all columns to CPU at once.
|
|
49
|
+
export class ComputeOutput {
|
|
50
|
+
constructor(dataGroup, regl) {
|
|
51
|
+
this._dataGroup = dataGroup
|
|
52
|
+
this._regl = regl
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
columns() {
|
|
56
|
+
return this._dataGroup.columns()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getData(col) {
|
|
60
|
+
const colData = this._dataGroup.getData(col)
|
|
61
|
+
if (!colData) return null
|
|
62
|
+
return new ReadableColumn(colData, this._regl)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getArrays() {
|
|
66
|
+
const result = {}
|
|
67
|
+
for (const col of this.columns()) {
|
|
68
|
+
const readable = this.getData(col)
|
|
69
|
+
if (readable) {
|
|
70
|
+
try {
|
|
71
|
+
result[col] = readable.getArray()
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(`[gladly] ComputeOutput.getArrays(): failed to read column '${col}': ${e.message}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Headless GPU compute pipeline for running data transforms without any visual output.
|
|
82
|
+
// Creates its own offscreen WebGL context; no DOM container needed.
|
|
83
|
+
//
|
|
84
|
+
// Usage:
|
|
85
|
+
// const pipeline = new ComputePipeline()
|
|
86
|
+
// const output = pipeline.update({ data, transforms, axes })
|
|
87
|
+
// const arr = output.getData('hist.counts').getArray() // Float32Array
|
|
88
|
+
// const all = output.getArrays() // { 'hist.counts': Float32Array, ... }
|
|
89
|
+
// pipeline.destroy()
|
|
90
|
+
export class ComputePipeline extends GlBase {
|
|
91
|
+
constructor() {
|
|
92
|
+
super()
|
|
93
|
+
const canvas = typeof OffscreenCanvas !== 'undefined'
|
|
94
|
+
? new OffscreenCanvas(1, 1)
|
|
95
|
+
: document.createElement('canvas')
|
|
96
|
+
this._initRegl(canvas)
|
|
97
|
+
this.filterAxisRegistry = new FilterAxisRegistry()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Runs the given transforms over data and returns a ComputeOutput.
|
|
101
|
+
//
|
|
102
|
+
// axes: { [quantityKind]: { min, max } } — sets filter axis ranges before computing.
|
|
103
|
+
// Transforms that access a filter axis will see the configured range.
|
|
104
|
+
async update({ data, transforms = [], axes = {} } = {}) {
|
|
105
|
+
const epoch = ++this._initEpoch
|
|
106
|
+
|
|
107
|
+
if (data !== undefined) {
|
|
108
|
+
this._rawData = normalizeData(data)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this._dataTransformNodes = []
|
|
112
|
+
this.filterAxisRegistry = new FilterAxisRegistry()
|
|
113
|
+
|
|
114
|
+
if (this._rawData != null) {
|
|
115
|
+
const fresh = new DataGroup({})
|
|
116
|
+
fresh._children = { ...this._rawData._children }
|
|
117
|
+
this.currentData = fresh
|
|
118
|
+
} else {
|
|
119
|
+
this.currentData = new DataGroup({})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Run transforms; filter axes are registered and data extents set during this step.
|
|
123
|
+
// At this point filter ranges are all null (open bounds).
|
|
124
|
+
await this._processTransforms(transforms, epoch)
|
|
125
|
+
if (this._initEpoch !== epoch) return new ComputeOutput(this.currentData, this.regl)
|
|
126
|
+
|
|
127
|
+
// Apply axes config to set filter ranges on any registered filter axis.
|
|
128
|
+
for (const [axisId, axisConfig] of Object.entries(axes)) {
|
|
129
|
+
if (this.filterAxisRegistry.hasAxis(axisId)) {
|
|
130
|
+
this.filterAxisRegistry.setRange(
|
|
131
|
+
axisId,
|
|
132
|
+
axisConfig.min !== undefined ? axisConfig.min : null,
|
|
133
|
+
axisConfig.max !== undefined ? axisConfig.max : null
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Refresh transforms whose output depends on any filter axis that now has a range set.
|
|
139
|
+
for (const node of this._dataTransformNodes) {
|
|
140
|
+
await node.refreshIfNeeded(this)
|
|
141
|
+
if (this._initEpoch !== epoch) return new ComputeOutput(this.currentData, this.regl)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return new ComputeOutput(this.currentData, this.regl)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
destroy() {
|
|
148
|
+
if (this.regl) {
|
|
149
|
+
this.regl.destroy()
|
|
150
|
+
this.regl = null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import reglInit from "regl"
|
|
2
|
+
import { FilterAxisRegistry } from "../axes/FilterAxisRegistry.js"
|
|
3
|
+
import { Axis } from "../axes/Axis.js"
|
|
4
|
+
import { DataGroup, ComputedDataNode } from "../data/Data.js"
|
|
5
|
+
import { getComputedData } from "../compute/ComputationRegistry.js"
|
|
6
|
+
|
|
7
|
+
export class GlBase {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.regl = null
|
|
10
|
+
this.currentData = null
|
|
11
|
+
this._rawData = null
|
|
12
|
+
this._dataTransformNodes = []
|
|
13
|
+
this.filterAxisRegistry = null
|
|
14
|
+
this._axisCache = new Map()
|
|
15
|
+
this._axesProxy = null
|
|
16
|
+
this._initEpoch = 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_initRegl(canvas) {
|
|
20
|
+
const gl = canvas.getContext('webgl2', { desynchronized: true })
|
|
21
|
+
if (!gl) throw new Error('WebGL 2.0 is required but not supported')
|
|
22
|
+
|
|
23
|
+
const origGetExtension = gl.getExtension.bind(gl)
|
|
24
|
+
gl.getExtension = (name) => {
|
|
25
|
+
const lname = name.toLowerCase()
|
|
26
|
+
const wgl2CoreExts = ['oes_texture_float', 'oes_texture_float_linear']
|
|
27
|
+
if (wgl2CoreExts.includes(lname)) return origGetExtension(name) ?? {}
|
|
28
|
+
if (lname === 'angle_instanced_arrays') {
|
|
29
|
+
return origGetExtension(name) ?? {
|
|
30
|
+
vertexAttribDivisorANGLE: gl.vertexAttribDivisor.bind(gl),
|
|
31
|
+
drawArraysInstancedANGLE: gl.drawArraysInstanced.bind(gl),
|
|
32
|
+
drawElementsInstancedANGLE: gl.drawElementsInstanced.bind(gl),
|
|
33
|
+
VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE: 0x88FE
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return origGetExtension(name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const GL_RGBA = 0x1908, GL_FLOAT = 0x1406, GL_RGBA32F = 0x8814
|
|
40
|
+
const origTexImage2D = gl.texImage2D.bind(gl)
|
|
41
|
+
gl.texImage2D = function (...args) {
|
|
42
|
+
if (args.length >= 8 && args[2] === GL_RGBA && args[7] === GL_FLOAT) {
|
|
43
|
+
args = [...args]
|
|
44
|
+
args[2] = GL_RGBA32F
|
|
45
|
+
}
|
|
46
|
+
return origTexImage2D(...args)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.regl = reglInit({
|
|
50
|
+
gl,
|
|
51
|
+
extensions: ['OES_texture_float', 'EXT_color_buffer_float', 'ANGLE_instanced_arrays'],
|
|
52
|
+
optionalExtensions: ['OES_texture_float_linear'],
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns a stable Axis instance for the given axis name, creating one on first access.
|
|
58
|
+
* The same instance is returned across update() calls so links survive updates.
|
|
59
|
+
*
|
|
60
|
+
* Usage: plot.axes.xaxis_bottom, pipeline.axes["velocity_ms"], etc.
|
|
61
|
+
*/
|
|
62
|
+
get axes() {
|
|
63
|
+
if (!this._axesProxy) {
|
|
64
|
+
this._axesProxy = new Proxy(this._axisCache, {
|
|
65
|
+
get: (cache, name) => {
|
|
66
|
+
if (typeof name !== 'string') return undefined
|
|
67
|
+
if (!cache.has(name)) cache.set(name, new Axis(this, name))
|
|
68
|
+
return cache.get(name)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
return this._axesProxy
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_getAxis(name) {
|
|
76
|
+
if (!this._axisCache.has(name)) this._axisCache.set(name, new Axis(this, name))
|
|
77
|
+
return this._axisCache.get(name)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For filter axes the axis ID is the quantity kind. Overridden by Plot for spatial axes.
|
|
81
|
+
getAxisQuantityKind(axisId) {
|
|
82
|
+
return axisId
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Default: filter axes only. Overridden by Plot to add spatial + color axes.
|
|
86
|
+
getAxisDomain(axisId) {
|
|
87
|
+
const filterRange = this.filterAxisRegistry?.getRange(axisId)
|
|
88
|
+
if (filterRange) return [filterRange.min, filterRange.max]
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default: filter axes only. Overridden by Plot to add spatial + color axes.
|
|
93
|
+
setAxisDomain(axisId, domain) {
|
|
94
|
+
if (this.filterAxisRegistry?.hasAxis(axisId)) {
|
|
95
|
+
this.filterAxisRegistry.setRange(axisId, domain[0], domain[1])
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// No-op in base class. Overridden by Plot to schedule a WebGL render frame.
|
|
100
|
+
scheduleRender() {}
|
|
101
|
+
|
|
102
|
+
async _processTransforms(transforms, epoch) {
|
|
103
|
+
if (!transforms || transforms.length === 0) return
|
|
104
|
+
|
|
105
|
+
const TDR_STEP_MS = 500
|
|
106
|
+
for (const { name, transform: spec } of transforms) {
|
|
107
|
+
const entries = Object.entries(spec)
|
|
108
|
+
if (entries.length !== 1) throw new Error(`Transform '${name}' must have exactly one key`)
|
|
109
|
+
const [className, params] = entries[0]
|
|
110
|
+
|
|
111
|
+
const computedData = getComputedData(className)
|
|
112
|
+
if (!computedData) throw new Error(`Unknown computed data type: '${className}'`)
|
|
113
|
+
|
|
114
|
+
const filterAxes = computedData.filterAxes(params, this.currentData)
|
|
115
|
+
for (const quantityKind of Object.values(filterAxes)) {
|
|
116
|
+
this.filterAxisRegistry.ensureFilterAxis(quantityKind)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const node = new ComputedDataNode(computedData, params)
|
|
120
|
+
const stepStart = performance.now()
|
|
121
|
+
try {
|
|
122
|
+
await node._initialize(this.regl, this.currentData, this)
|
|
123
|
+
} catch (e) {
|
|
124
|
+
throw new Error(`Transform '${name}' (${className}) failed to initialize: ${e.message}`, { cause: e })
|
|
125
|
+
}
|
|
126
|
+
if (performance.now() - stepStart > TDR_STEP_MS)
|
|
127
|
+
await new Promise(r => requestAnimationFrame(r))
|
|
128
|
+
if (this._initEpoch !== epoch) return
|
|
129
|
+
|
|
130
|
+
const filterDataExtents = node._meta?.filterDataExtents ?? {}
|
|
131
|
+
for (const [qk, extent] of Object.entries(filterDataExtents)) {
|
|
132
|
+
if (this.filterAxisRegistry.hasAxis(qk)) {
|
|
133
|
+
this.filterAxisRegistry.setDataExtent(qk, extent[0], extent[1])
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.currentData._children[name] = node
|
|
138
|
+
this._dataTransformNodes.push(node)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class Layer {
|
|
2
|
+
constructor({ type, attributes, uniforms, domains = {}, lineWidth = 1, primitive = "points", xAxis = "xaxis_bottom", yAxis = "yaxis_left", zAxis = null, xAxisQuantityKind, yAxisQuantityKind, zAxisQuantityKind, colorAxes = {}, colorAxes2d = {}, filterAxes = {}, vertexCount = null, instanceCount = null, attributeDivisors = {}, blend = null }) {
|
|
3
|
+
// Validate that all attributes are non-null/undefined
|
|
4
|
+
// (Float32Array, regl textures, numbers, and expression objects are all valid)
|
|
5
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
6
|
+
if (value == null) {
|
|
7
|
+
throw new Error(`Attribute '${key}' must not be null or undefined`)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Validate colorAxes: must be a dict mapping GLSL name suffix to quantity kind string
|
|
12
|
+
for (const quantityKind of Object.values(colorAxes)) {
|
|
13
|
+
if (typeof quantityKind !== 'string') {
|
|
14
|
+
throw new Error(`Color axis quantity kind must be a string, got ${typeof quantityKind}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Validate filterAxes: must be a dict mapping GLSL name suffix to quantity kind string
|
|
19
|
+
for (const quantityKind of Object.values(filterAxes)) {
|
|
20
|
+
if (typeof quantityKind !== 'string') {
|
|
21
|
+
throw new Error(`Filter axis quantity kind must be a string, got ${typeof quantityKind}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!type?.suppressWarnings) {
|
|
26
|
+
if (vertexCount !== null && vertexCount === 0) {
|
|
27
|
+
console.warn(`[gladly] Layer '${type?.name ?? 'unknown'}': vertexCount is 0 — this layer will draw nothing`)
|
|
28
|
+
}
|
|
29
|
+
if (instanceCount !== null && instanceCount === 0) {
|
|
30
|
+
console.warn(`[gladly] Layer '${type?.name ?? 'unknown'}': instanceCount is 0 — this layer will draw nothing`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.type = type
|
|
35
|
+
this.attributes = attributes
|
|
36
|
+
this.uniforms = uniforms
|
|
37
|
+
this.domains = domains
|
|
38
|
+
this.lineWidth = lineWidth
|
|
39
|
+
this.primitive = primitive
|
|
40
|
+
this.xAxis = xAxis
|
|
41
|
+
this.yAxis = yAxis
|
|
42
|
+
this.zAxis = zAxis
|
|
43
|
+
this.xAxisQuantityKind = xAxisQuantityKind
|
|
44
|
+
this.yAxisQuantityKind = yAxisQuantityKind
|
|
45
|
+
this.zAxisQuantityKind = zAxisQuantityKind
|
|
46
|
+
// colorAxes: Record<suffix, qk> — maps GLSL name suffix to quantity kind for each color axis
|
|
47
|
+
// e.g. { '': 'temperature_K' } or { '': 'temp_K', '2': 'pressure_Pa' }
|
|
48
|
+
this.colorAxes = colorAxes
|
|
49
|
+
// colorAxes2d: Record<suffix2d, [suffix1, suffix2]> — maps a 2D function name suffix to a pair
|
|
50
|
+
// of colorAxes suffixes; generates map_color_2d_SUFFIX(vec2) GLSL wrapper
|
|
51
|
+
this.colorAxes2d = colorAxes2d
|
|
52
|
+
// filterAxes: Record<suffix, qk> — maps GLSL name suffix to quantity kind for each filter axis
|
|
53
|
+
this.filterAxes = filterAxes
|
|
54
|
+
this.vertexCount = vertexCount
|
|
55
|
+
this.instanceCount = instanceCount
|
|
56
|
+
this.attributeDivisors = attributeDivisors
|
|
57
|
+
this.blend = blend
|
|
58
|
+
}
|
|
59
|
+
}
|