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.
Files changed (60) hide show
  1. package/README.md +9 -2
  2. package/package.json +10 -11
  3. package/src/axes/Axis.js +401 -0
  4. package/src/{AxisLink.js → axes/AxisLink.js} +6 -2
  5. package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
  6. package/src/axes/AxisRegistry.js +179 -0
  7. package/src/axes/Camera.js +47 -0
  8. package/src/axes/ColorAxisRegistry.js +101 -0
  9. package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
  10. package/src/axes/TickLabelAtlas.js +99 -0
  11. package/src/axes/ZoomController.js +463 -0
  12. package/src/colorscales/BivariateColorscales.js +205 -0
  13. package/src/colorscales/ColorscaleRegistry.js +144 -0
  14. package/src/compute/ComputationRegistry.js +179 -0
  15. package/src/compute/axisFilter.js +59 -0
  16. package/src/compute/conv.js +286 -0
  17. package/src/compute/elementwise.js +72 -0
  18. package/src/compute/fft.js +378 -0
  19. package/src/compute/filter.js +229 -0
  20. package/src/compute/hist.js +285 -0
  21. package/src/compute/kde.js +120 -0
  22. package/src/compute/scatter2dInterpolate.js +277 -0
  23. package/src/compute/util.js +196 -0
  24. package/src/core/ComputePipeline.js +153 -0
  25. package/src/core/GlBase.js +141 -0
  26. package/src/core/Layer.js +59 -0
  27. package/src/core/LayerType.js +433 -0
  28. package/src/core/Plot.js +1213 -0
  29. package/src/core/PlotGroup.js +204 -0
  30. package/src/core/ShaderQueue.js +73 -0
  31. package/src/data/ColumnData.js +269 -0
  32. package/src/data/Computation.js +95 -0
  33. package/src/data/Data.js +270 -0
  34. package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
  35. package/src/floats/Colorbar2d.js +77 -0
  36. package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
  37. package/src/{FilterbarFloat.js → floats/Float.js} +73 -30
  38. package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
  39. package/src/index.js +47 -22
  40. package/src/layers/BarsLayer.js +168 -0
  41. package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +12 -16
  42. package/src/layers/ColorbarLayer2d.js +86 -0
  43. package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +6 -5
  44. package/src/layers/LinesLayer.js +185 -0
  45. package/src/layers/PointsLayer.js +118 -0
  46. package/src/layers/ScatterShared.js +98 -0
  47. package/src/{TileLayer.js → layers/TileLayer.js} +24 -20
  48. package/src/math/mat4.js +100 -0
  49. package/src/Axis.js +0 -48
  50. package/src/AxisRegistry.js +0 -54
  51. package/src/ColorAxisRegistry.js +0 -49
  52. package/src/ColorscaleRegistry.js +0 -52
  53. package/src/Data.js +0 -67
  54. package/src/Float.js +0 -159
  55. package/src/Layer.js +0 -44
  56. package/src/LayerType.js +0 -209
  57. package/src/Plot.js +0 -1073
  58. package/src/ScatterLayer.js +0 -287
  59. /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
  60. /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
+ }