@bigmistqke/view.gl 0.1.0 → 0.1.2

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 (52) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.github/ISSUE_TEMPLATE/bug-report.yml +31 -0
  3. package/.github/ISSUE_TEMPLATE/feature-request.yml +27 -0
  4. package/.github/workflows/codeql.yml +46 -0
  5. package/.github/workflows/deploy-page.yml +38 -0
  6. package/.github/workflows/format.yml +40 -0
  7. package/README.md +92 -35
  8. package/dev/404.html +19 -0
  9. package/dev/dist/404.html +19 -0
  10. package/dev/dist/assets/bezier-DuLTb2d1.js +294 -0
  11. package/dev/dist/assets/game_of_life-D7qHdFcv.js +202 -0
  12. package/dev/dist/assets/grid-Ie22U_9K.js +173 -0
  13. package/dev/dist/assets/index-0qrXMlfv.js +109 -0
  14. package/dev/dist/assets/index-C2N3u8wj.css +12 -0
  15. package/dev/dist/assets/main-BZ1N6pp6.js +32 -0
  16. package/dev/dist/assets/overview-DAxtMIw5.js +78 -0
  17. package/dev/dist/assets/pix_sim-DIhzM_vu.js +501 -0
  18. package/dev/dist/assets/symbol-BouRL-Ni.js +65 -0
  19. package/dev/dist/assets/tag-C_bg62fD.js +516 -0
  20. package/dev/dist/assets/tetris-IMnkSi9p.js +344 -0
  21. package/dev/dist/assets/utils-CvCgsM08.js +50 -0
  22. package/dev/dist/index.html +51 -0
  23. package/dev/index.html +39 -10
  24. package/dev/{bezier.ts → src/examples/bezier.ts} +1 -1
  25. package/dev/{grid.ts → src/examples/grid.ts} +1 -1
  26. package/dev/{pix_sim.ts → src/examples/pix_sim.ts} +112 -97
  27. package/dev/src/examples/symbol.ts +79 -0
  28. package/dev/{tetris.ts → src/examples/tetris.ts} +1 -0
  29. package/dev/src/index.css +12 -0
  30. package/dev/src/index.ts +69 -0
  31. package/dev/{utils.ts → src/utils.ts} +8 -2
  32. package/dev/vite.config.ts +9 -1
  33. package/dist/index.d.mts +18 -0
  34. package/dist/index.mjs +324 -0
  35. package/dist/index.mjs.map +1 -0
  36. package/dist/tag.d.mts +97 -0
  37. package/dist/tag.mjs +162 -0
  38. package/dist/tag.mjs.map +1 -0
  39. package/dist/types-DoFAJoh-.d.mts +266 -0
  40. package/package.json +24 -10
  41. package/src/index.ts +39 -17
  42. package/src/tag.ts +47 -39
  43. package/src/types.ts +24 -24
  44. package/src/{utils.ts → utils/index.ts} +39 -14
  45. package/src/utils/object.ts +27 -0
  46. package/test/index.test.ts +1017 -0
  47. package/test/mocks/webgl.ts +248 -0
  48. package/test/setup.ts +13 -0
  49. package/test/tag.test.ts +790 -0
  50. package/vitest.config.ts +15 -0
  51. /package/dev/{game_of_life.ts → src/examples/game_of_life.ts} +0 -0
  52. /package/dev/{index.ts → src/examples/overview.ts} +0 -0
@@ -0,0 +1,1017 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ attributeView,
4
+ bufferView,
5
+ interleavedAttributeView,
6
+ uniformView,
7
+ view,
8
+ } from '../src/index'
9
+ import type {
10
+ AttributeSchema,
11
+ BufferSchema,
12
+ InterleavedAttributeSchema,
13
+ UniformSchema,
14
+ ViewSchema,
15
+ } from '../src/types'
16
+ import { createMockCanvas, createMockGL } from './mocks/webgl'
17
+
18
+ describe('view', () => {
19
+ let gl: WebGL2RenderingContext
20
+ let program: WebGLProgram
21
+ beforeEach(() => {
22
+ const mock = createMockCanvas()
23
+ gl = mock.gl
24
+ program = gl.createProgram()!
25
+ })
26
+
27
+ it('should create a view with uniforms', () => {
28
+ const schema = {
29
+ uniforms: {
30
+ u_resolution: { kind: 'vec2' },
31
+ u_time: { kind: 'float' },
32
+ u_texture: { kind: 'sampler2D' },
33
+ },
34
+ } satisfies ViewSchema
35
+
36
+ const result = view(gl, program, schema)
37
+
38
+ expect(result.uniforms).toBeDefined()
39
+ expect(result.uniforms!.u_resolution).toBeDefined()
40
+ expect(result.uniforms!.u_resolution.set).toBeInstanceOf(Function)
41
+ expect(result.uniforms!.u_time).toBeDefined()
42
+ expect(result.uniforms!.u_time.set).toBeInstanceOf(Function)
43
+ expect(result.uniforms!.u_texture).toBeDefined()
44
+ expect(result.uniforms!.u_texture.set).toBeInstanceOf(Function)
45
+ expect(result.attributes).toBeUndefined()
46
+ expect(result.interleavedAttributes).toBeUndefined()
47
+ expect(result.buffers).toBeUndefined()
48
+ })
49
+
50
+ it('should create a view with attributes', () => {
51
+ const schema = {
52
+ attributes: {
53
+ a_position: { kind: 'vec2' },
54
+ a_color: { kind: 'vec4', instanced: true },
55
+ },
56
+ } satisfies ViewSchema
57
+
58
+ const result = view(gl, program, schema)
59
+
60
+ expect(result.attributes).toBeDefined()
61
+ expect(result.attributes!.a_position).toBeDefined()
62
+ expect(result.attributes!.a_position.buffer).toBeDefined()
63
+ expect(result.attributes!.a_position.bind).toBeInstanceOf(Function)
64
+ expect(result.attributes!.a_position.dispose).toBeInstanceOf(Function)
65
+ expect(result.attributes!.a_position.set).toBeInstanceOf(Function)
66
+ expect(result.uniforms).toBeUndefined()
67
+ })
68
+
69
+ it('should create a view with interleaved attributes', () => {
70
+ const schema = {
71
+ interleavedAttributes: {
72
+ data: {
73
+ layout: [
74
+ { key: 'a_position', kind: 'vec2' },
75
+ { key: 'a_color', kind: 'vec4' },
76
+ ],
77
+ instanced: false,
78
+ },
79
+ },
80
+ } satisfies ViewSchema
81
+
82
+ const result = view(gl, program, schema)
83
+
84
+ expect(result.interleavedAttributes).toBeDefined()
85
+ expect(result.interleavedAttributes!.data).toBeDefined()
86
+ expect(result.interleavedAttributes!.data.bind).toBeInstanceOf(Function)
87
+ expect(result.interleavedAttributes!.data.unbind).toBeInstanceOf(Function)
88
+ expect(result.interleavedAttributes!.data.dispose).toBeInstanceOf(Function)
89
+ expect(result.interleavedAttributes!.data.set).toBeInstanceOf(Function)
90
+ })
91
+
92
+ it('should create a view with buffers', () => {
93
+ const schema = {
94
+ buffers: {
95
+ vertices: { target: 'ARRAY_BUFFER', usage: 'STATIC_DRAW' },
96
+ indices: { target: 'ELEMENT_ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
97
+ },
98
+ } satisfies ViewSchema
99
+
100
+ const result = view(gl, program, schema)
101
+
102
+ expect(result.buffers).toBeDefined()
103
+ expect(result.buffers!.vertices).toBeDefined()
104
+ expect(result.buffers!.vertices.bind).toBeInstanceOf(Function)
105
+ expect(result.buffers!.vertices.dispose).toBeInstanceOf(Function)
106
+ expect(result.buffers!.vertices.set).toBeInstanceOf(Function)
107
+ })
108
+
109
+ it('should create a view with all components', () => {
110
+ const schema = {
111
+ uniforms: {
112
+ u_time: { kind: 'float' },
113
+ },
114
+ attributes: {
115
+ a_position: { kind: 'vec2' },
116
+ },
117
+ interleavedAttributes: {
118
+ data: {
119
+ layout: [{ key: 'a_uv', kind: 'vec2' }],
120
+ instanced: false,
121
+ },
122
+ },
123
+ buffers: {
124
+ vertices: {},
125
+ },
126
+ } satisfies ViewSchema
127
+
128
+ const result = view(gl, program, schema)
129
+
130
+ expect(result.uniforms).toBeDefined()
131
+ expect(result.attributes).toBeDefined()
132
+ expect(result.interleavedAttributes).toBeDefined()
133
+ expect(result.buffers).toBeDefined()
134
+ })
135
+
136
+ it('should create a view with symbols for all component types', () => {
137
+ const u_time_symbol = Symbol('u_time')
138
+ const a_position_symbol = Symbol('a_position')
139
+ const data_symbol = Symbol('data')
140
+ const vertices_symbol = Symbol('vertices')
141
+
142
+ const schema = {
143
+ uniforms: {
144
+ [u_time_symbol]: { kind: 'float' },
145
+ },
146
+ attributes: {
147
+ [a_position_symbol]: { kind: 'vec2' },
148
+ },
149
+ interleavedAttributes: {
150
+ [data_symbol]: {
151
+ layout: [{ key: 'a_uv', kind: 'vec2' }],
152
+ instanced: false,
153
+ },
154
+ },
155
+ buffers: {
156
+ [vertices_symbol]: {},
157
+ },
158
+ } satisfies ViewSchema
159
+
160
+ const result = view(gl, program, schema)
161
+
162
+ expect(result.uniforms![u_time_symbol]).toBeDefined()
163
+ expect(result.uniforms![u_time_symbol].set).toBeInstanceOf(Function)
164
+
165
+ expect(result.attributes![a_position_symbol]).toBeDefined()
166
+ expect(result.attributes![a_position_symbol].bind).toBeInstanceOf(Function)
167
+
168
+ expect(result.interleavedAttributes![data_symbol]).toBeDefined()
169
+ expect(result.interleavedAttributes![data_symbol].bind).toBeInstanceOf(Function)
170
+
171
+ expect(result.buffers![vertices_symbol]).toBeDefined()
172
+ expect(result.buffers![vertices_symbol].bind).toBeInstanceOf(Function)
173
+ })
174
+
175
+ it('should create a view with mixed string and symbol keys', () => {
176
+ const u_color_symbol = Symbol('u_color')
177
+ const a_normal_symbol = Symbol('a_normal')
178
+
179
+ const schema = {
180
+ uniforms: {
181
+ u_time: { kind: 'float' },
182
+ [u_color_symbol]: { kind: 'vec3' },
183
+ },
184
+ attributes: {
185
+ a_position: { kind: 'vec3' },
186
+ [a_normal_symbol]: { kind: 'vec3' },
187
+ },
188
+ } satisfies ViewSchema
189
+
190
+ const result = view(gl, program, schema)
191
+
192
+ // String keys
193
+ expect(result.uniforms!.u_time).toBeDefined()
194
+ expect(result.attributes!.a_position).toBeDefined()
195
+
196
+ // Symbol keys
197
+ expect(result.uniforms![u_color_symbol]).toBeDefined()
198
+ expect(result.attributes![a_normal_symbol]).toBeDefined()
199
+ })
200
+ })
201
+
202
+ describe('uniformView', () => {
203
+ let gl: ReturnType<typeof createMockGL>
204
+ let program: WebGLProgram
205
+
206
+ beforeEach(() => {
207
+ gl = createMockGL()
208
+ program = gl.createProgram()!
209
+ })
210
+
211
+ it('should create uniform setters for basic types', () => {
212
+ const schema = {
213
+ u_float: { kind: 'float' },
214
+ u_vec2: { kind: 'vec2' },
215
+ u_vec3: { kind: 'vec3' },
216
+ u_vec4: { kind: 'vec4' },
217
+ } satisfies UniformSchema
218
+
219
+ const uniforms = uniformView(gl, program, schema)
220
+
221
+ // Test float uniform
222
+ uniforms.u_float.set(1.5)
223
+ expect(gl.uniform1f).toHaveBeenCalledWith(expect.any(Object), 1.5)
224
+
225
+ // Test vec2 uniform
226
+ uniforms.u_vec2.set(2.0, 3.0)
227
+ expect(gl.uniform2f).toHaveBeenCalledWith(expect.any(Object), 2.0, 3.0)
228
+
229
+ // Test vec3 uniform
230
+ uniforms.u_vec3.set(1.0, 2.0, 3.0)
231
+ expect(gl.uniform3f).toHaveBeenCalledWith(expect.any(Object), 1.0, 2.0, 3.0)
232
+
233
+ // Test vec4 uniform
234
+ uniforms.u_vec4.set(1.0, 2.0, 3.0, 4.0)
235
+ expect(gl.uniform4f).toHaveBeenCalledWith(expect.any(Object), 1.0, 2.0, 3.0, 4.0)
236
+ })
237
+
238
+ it('should create uniform setters for array types', () => {
239
+ const schema = {
240
+ u_floatArray: { kind: 'float', size: 5 },
241
+ u_vec3Array: { kind: 'vec3', size: 3 },
242
+ } satisfies UniformSchema
243
+
244
+ const uniforms = uniformView(gl, program, schema)
245
+
246
+ // Test float array
247
+ const floatArray = new Float32Array([1, 2, 3, 4, 5])
248
+ uniforms.u_floatArray.set(floatArray)
249
+ expect(gl.uniform1fv).toHaveBeenCalledWith(expect.any(Object), floatArray)
250
+
251
+ // Test vec3 array
252
+ const vec3Array = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9])
253
+ uniforms.u_vec3Array.set(vec3Array)
254
+ expect(gl.uniform3fv).toHaveBeenCalledWith(expect.any(Object), vec3Array)
255
+ })
256
+
257
+ it('should create uniform setters for matrix types', () => {
258
+ const schema = {
259
+ u_mat2: { kind: 'mat2' },
260
+ u_mat3: { kind: 'mat3' },
261
+ u_mat4: { kind: 'mat4' },
262
+ } satisfies UniformSchema
263
+
264
+ const uniforms = uniformView(gl, program, schema)
265
+
266
+ // Test mat2
267
+ const mat2 = new Float32Array([1, 2, 3, 4])
268
+ uniforms.u_mat2.set(mat2)
269
+ expect(gl.uniformMatrix2fv).toHaveBeenCalledWith(expect.any(Object), false, mat2)
270
+
271
+ // Test mat3
272
+ const mat3 = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9])
273
+ uniforms.u_mat3.set(mat3)
274
+ expect(gl.uniformMatrix3fv).toHaveBeenCalledWith(expect.any(Object), false, mat3)
275
+
276
+ // Test mat4
277
+ const mat4 = new Float32Array(16)
278
+ uniforms.u_mat4.set(mat4)
279
+ expect(gl.uniformMatrix4fv).toHaveBeenCalledWith(expect.any(Object), false, mat4)
280
+ })
281
+
282
+ it('should create uniform setters for sampler types', () => {
283
+ const schema = {
284
+ u_texture: { kind: 'sampler2D' },
285
+ u_cubeMap: { kind: 'samplerCube' },
286
+ } satisfies UniformSchema
287
+
288
+ const uniforms = uniformView(gl, program, schema)
289
+
290
+ // Test sampler2D
291
+ uniforms.u_texture.set(0)
292
+ expect(gl.uniform1i).toHaveBeenCalledWith(expect.any(Object), 0)
293
+
294
+ // Test samplerCube
295
+ uniforms.u_cubeMap.set(1)
296
+ expect(gl.uniform1i).toHaveBeenCalledWith(expect.any(Object), 1)
297
+ })
298
+
299
+ it('should create uniform setters for integer types', () => {
300
+ const schema = {
301
+ u_int: { kind: 'int' },
302
+ u_ivec2: { kind: 'ivec2' },
303
+ u_ivec3: { kind: 'ivec3' },
304
+ u_ivec4: { kind: 'ivec4' },
305
+ } satisfies UniformSchema
306
+
307
+ const uniforms = uniformView(gl, program, schema)
308
+
309
+ // Test int uniform
310
+ uniforms.u_int.set(42)
311
+ expect(gl.uniform1i).toHaveBeenCalledWith(expect.any(Object), 42)
312
+
313
+ // Test ivec2 uniform
314
+ uniforms.u_ivec2.set(1, 2)
315
+ expect(gl.uniform2i).toHaveBeenCalledWith(expect.any(Object), 1, 2)
316
+
317
+ // Test ivec3 uniform
318
+ uniforms.u_ivec3.set(1, 2, 3)
319
+ expect(gl.uniform3i).toHaveBeenCalledWith(expect.any(Object), 1, 2, 3)
320
+
321
+ // Test ivec4 uniform
322
+ uniforms.u_ivec4.set(1, 2, 3, 4)
323
+ expect(gl.uniform4i).toHaveBeenCalledWith(expect.any(Object), 1, 2, 3, 4)
324
+ })
325
+
326
+ it('should support symbols as uniform keys', () => {
327
+ const u_time_symbol = Symbol('u_time')
328
+ const u_color_symbol = Symbol('u_color')
329
+
330
+ const schema = {
331
+ [u_time_symbol]: { kind: 'float' },
332
+ [u_color_symbol]: { kind: 'vec3' },
333
+ } satisfies UniformSchema
334
+
335
+ const uniforms = uniformView(gl, program, schema)
336
+
337
+ expect(uniforms[u_time_symbol]).toBeDefined()
338
+ expect(uniforms[u_time_symbol].set).toBeInstanceOf(Function)
339
+ expect(uniforms[u_color_symbol]).toBeDefined()
340
+ expect(uniforms[u_color_symbol].set).toBeInstanceOf(Function)
341
+
342
+ // Test that we can call the methods
343
+ uniforms[u_time_symbol].set(1.5)
344
+ expect(gl.uniform1f).toHaveBeenCalledWith(expect.any(Object), 1.5)
345
+
346
+ uniforms[u_color_symbol].set(1.0, 0.0, 0.5)
347
+ expect(gl.uniform3f).toHaveBeenCalledWith(expect.any(Object), 1.0, 0.0, 0.5)
348
+ })
349
+
350
+ it('should support symbols in uniform arrays', () => {
351
+ const u_positions_symbol = Symbol('u_positions')
352
+
353
+ const schema = {
354
+ [u_positions_symbol]: { kind: 'vec3', size: 10 },
355
+ } satisfies UniformSchema
356
+
357
+ const uniforms = uniformView(gl, program, schema)
358
+
359
+ expect(uniforms[u_positions_symbol]).toBeDefined()
360
+ const data = new Float32Array(30)
361
+ uniforms[u_positions_symbol].set(data)
362
+ expect(gl.uniform3fv).toHaveBeenCalledWith(expect.any(Object), data)
363
+ })
364
+ })
365
+
366
+ describe('attributeView', () => {
367
+ let gl: ReturnType<typeof createMockGL>
368
+ let program: WebGLProgram
369
+
370
+ beforeEach(() => {
371
+ gl = createMockGL()
372
+ program = gl.createProgram()!
373
+ })
374
+
375
+ it('should create attribute methods for basic types', () => {
376
+ const schema = {
377
+ a_position: { kind: 'vec2' },
378
+ a_color: { kind: 'vec4' },
379
+ } satisfies AttributeSchema
380
+
381
+ const attributes = attributeView(gl, program, schema)
382
+
383
+ expect(attributes.a_position).toBeDefined()
384
+ expect(attributes.a_position.buffer).toBeDefined()
385
+ expect(gl.createBuffer).toHaveBeenCalled()
386
+
387
+ // Test bind
388
+ attributes.a_position.bind()
389
+ expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ARRAY_BUFFER, attributes.a_position.buffer)
390
+ expect(gl.enableVertexAttribArray).toHaveBeenCalled()
391
+ expect(gl.vertexAttribPointer).toHaveBeenCalledWith(
392
+ expect.any(Number), // location
393
+ 2, // size for vec2
394
+ gl.FLOAT,
395
+ false,
396
+ 0, // stride
397
+ 0, // offset
398
+ )
399
+ })
400
+
401
+ it('should handle instanced attributes', () => {
402
+ const schema = {
403
+ a_instancePosition: { kind: 'vec3', instanced: true },
404
+ } satisfies AttributeSchema
405
+
406
+ const attributes = attributeView(gl, program, schema)
407
+
408
+ attributes.a_instancePosition.bind()
409
+ expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 1)
410
+ })
411
+
412
+ it('should use custom buffer if provided', () => {
413
+ const customBuffer = gl.createBuffer()!
414
+ const schema = {
415
+ a_position: { kind: 'vec2', buffer: customBuffer },
416
+ } satisfies AttributeSchema
417
+
418
+ const attributes = attributeView(gl, program, schema)
419
+
420
+ expect(attributes.a_position.buffer).toBe(customBuffer)
421
+ // Should not create a new buffer
422
+ expect(gl.createBuffer).toHaveBeenCalledTimes(1) // Only for the custom buffer
423
+ })
424
+
425
+ it('should handle float attributes', () => {
426
+ const schema = {
427
+ a_scalar: { kind: 'float' },
428
+ } satisfies AttributeSchema
429
+
430
+ const attributes = attributeView(gl, program, schema)
431
+
432
+ attributes.a_scalar.bind()
433
+ expect(gl.vertexAttribPointer).toHaveBeenCalledWith(
434
+ expect.any(Number),
435
+ 1, // size for float
436
+ gl.FLOAT,
437
+ false,
438
+ 0,
439
+ 0,
440
+ )
441
+ })
442
+
443
+ it('should handle integer attributes', () => {
444
+ const schema = {
445
+ a_index: { kind: 'int' },
446
+ a_indices: { kind: 'ivec2' },
447
+ } satisfies AttributeSchema
448
+
449
+ const attributes = attributeView(gl, program, schema)
450
+
451
+ attributes.a_index.bind()
452
+ expect(gl.vertexAttribPointer).toHaveBeenCalledWith(
453
+ expect.any(Number),
454
+ 1, // size for int
455
+ gl.INT,
456
+ false,
457
+ 0,
458
+ 0,
459
+ )
460
+
461
+ attributes.a_indices.bind()
462
+ expect(gl.vertexAttribPointer).toHaveBeenCalledWith(
463
+ expect.any(Number),
464
+ 2, // size for ivec2
465
+ gl.INT,
466
+ false,
467
+ 0,
468
+ 0,
469
+ )
470
+ })
471
+
472
+ it('should set buffer data', () => {
473
+ const schema = {
474
+ a_position: { kind: 'vec2' },
475
+ } satisfies AttributeSchema
476
+
477
+ const attributes = attributeView(gl, program, schema)
478
+ const data = new Float32Array([1, 2, 3, 4, 5, 6])
479
+
480
+ const result = attributes.a_position.set(data)
481
+ expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ARRAY_BUFFER, attributes.a_position.buffer)
482
+ expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
483
+ expect(result).toBe(attributes.a_position) // Should return this for chaining
484
+ })
485
+
486
+ it('should set buffer data with custom usage', () => {
487
+ const schema = {
488
+ a_position: { kind: 'vec2' },
489
+ } satisfies AttributeSchema
490
+
491
+ const attributes = attributeView(gl, program, schema)
492
+ const data = new Float32Array([1, 2, 3, 4])
493
+
494
+ attributes.a_position.set(data, 'DYNAMIC_DRAW')
495
+ expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW)
496
+ })
497
+
498
+ it('should dispose buffers', () => {
499
+ const schema = {
500
+ a_position: { kind: 'vec2' },
501
+ a_color: { kind: 'vec4' },
502
+ } satisfies AttributeSchema
503
+
504
+ const attributes = attributeView(gl, program, schema)
505
+
506
+ attributes.a_position.dispose()
507
+ expect(gl.deleteBuffer).toHaveBeenCalledWith(attributes.a_position.buffer)
508
+
509
+ attributes.a_color.dispose()
510
+ expect(gl.deleteBuffer).toHaveBeenCalledWith(attributes.a_color.buffer)
511
+ })
512
+
513
+ it('should dispose all buffers on abort signal', () => {
514
+ const controller = new AbortController()
515
+ const schema = {
516
+ a_position: { kind: 'vec2' },
517
+ a_color: { kind: 'vec4' },
518
+ } satisfies AttributeSchema
519
+
520
+ const attributes = attributeView(gl, program, schema, { signal: controller.signal })
521
+
522
+ controller.abort()
523
+
524
+ expect(gl.deleteBuffer).toHaveBeenCalledWith(attributes.a_position.buffer)
525
+ expect(gl.deleteBuffer).toHaveBeenCalledWith(attributes.a_color.buffer)
526
+ })
527
+
528
+ it('should throw error if attribute not found in program', () => {
529
+ gl.getAttribLocation = vi.fn(() => -1)
530
+
531
+ const schema = {
532
+ a_notFound: { kind: 'vec2' },
533
+ } satisfies AttributeSchema
534
+
535
+ expect(() => attributeView(gl, program, schema)).toThrow("Attribute 'a_notFound' not found")
536
+ })
537
+
538
+ it('should support symbols as attribute keys', () => {
539
+ const a_position_symbol = Symbol('a_position')
540
+ const a_color_symbol = Symbol('a_color')
541
+
542
+ const schema = {
543
+ [a_position_symbol]: { kind: 'vec2' },
544
+ [a_color_symbol]: { kind: 'vec4', instanced: true },
545
+ } satisfies AttributeSchema
546
+
547
+ const attributes = attributeView(gl, program, schema)
548
+
549
+ expect(attributes[a_position_symbol]).toBeDefined()
550
+ expect(attributes[a_position_symbol].buffer).toBeDefined()
551
+ expect(attributes[a_position_symbol].bind).toBeInstanceOf(Function)
552
+ expect(attributes[a_position_symbol].set).toBeInstanceOf(Function)
553
+ expect(attributes[a_position_symbol].dispose).toBeInstanceOf(Function)
554
+
555
+ expect(attributes[a_color_symbol]).toBeDefined()
556
+
557
+ // Test binding
558
+ attributes[a_position_symbol].bind()
559
+ expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ARRAY_BUFFER, attributes[a_position_symbol].buffer)
560
+ expect(gl.vertexAttribPointer).toHaveBeenCalledWith(
561
+ expect.any(Number),
562
+ 2, // size for vec2
563
+ gl.FLOAT,
564
+ false,
565
+ 0,
566
+ 0,
567
+ )
568
+
569
+ // Test instanced attribute
570
+ attributes[a_color_symbol].bind()
571
+ expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 1)
572
+ })
573
+
574
+ it('should support symbols with custom buffers', () => {
575
+ const a_vertex_symbol = Symbol('a_vertex')
576
+ const customBuffer = gl.createBuffer()!
577
+
578
+ const schema = {
579
+ [a_vertex_symbol]: { kind: 'vec3', buffer: customBuffer },
580
+ } satisfies AttributeSchema
581
+
582
+ const attributes = attributeView(gl, program, schema)
583
+
584
+ expect(attributes[a_vertex_symbol].buffer).toBe(customBuffer)
585
+ })
586
+ })
587
+
588
+ describe('interleavedAttributeView', () => {
589
+ let gl: ReturnType<typeof createMockGL>
590
+ let program: WebGLProgram
591
+
592
+ beforeEach(() => {
593
+ gl = createMockGL()
594
+ program = gl.createProgram()!
595
+ })
596
+
597
+ it('should create interleaved attribute methods', () => {
598
+ const schema = {
599
+ vertexData: {
600
+ layout: [
601
+ { key: 'a_position', kind: 'vec2' },
602
+ { key: 'a_color', kind: 'vec4' },
603
+ ],
604
+ instanced: false,
605
+ },
606
+ } satisfies InterleavedAttributeSchema
607
+
608
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
609
+
610
+ expect(interleavedAttributes.vertexData).toBeDefined()
611
+ expect(interleavedAttributes.vertexData.bind).toBeInstanceOf(Function)
612
+ expect(interleavedAttributes.vertexData.unbind).toBeInstanceOf(Function)
613
+ expect(interleavedAttributes.vertexData.dispose).toBeInstanceOf(Function)
614
+ expect(interleavedAttributes.vertexData.set).toBeInstanceOf(Function)
615
+ })
616
+
617
+ it('should calculate correct stride and offsets', () => {
618
+ const schema = {
619
+ vertexData: {
620
+ layout: [
621
+ { key: 'a_position', kind: 'vec3' }, // 3 * 4 = 12 bytes
622
+ { key: 'a_normal', kind: 'vec3' }, // 3 * 4 = 12 bytes
623
+ { key: 'a_uv', kind: 'vec2' }, // 2 * 4 = 8 bytes
624
+ ], // Total stride = 32 bytes
625
+ instanced: false,
626
+ },
627
+ } satisfies InterleavedAttributeSchema
628
+
629
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
630
+
631
+ interleavedAttributes.vertexData.bind()
632
+
633
+ // Check vertexAttribPointer calls
634
+ const calls = (gl.vertexAttribPointer as any).mock.calls
635
+
636
+ // First attribute (position)
637
+ expect(calls[0][1]).toBe(3) // size
638
+ expect(calls[0][3]).toBe(false) // normalized
639
+ expect(calls[0][4]).toBe(32) // stride
640
+ expect(calls[0][5]).toBe(0) // offset
641
+
642
+ // Second attribute (normal)
643
+ expect(calls[1][1]).toBe(3) // size
644
+ expect(calls[1][3]).toBe(false) // normalized
645
+ expect(calls[1][4]).toBe(32) // stride
646
+ expect(calls[1][5]).toBe(12) // offset
647
+
648
+ // Third attribute (uv)
649
+ expect(calls[2][1]).toBe(2) // size
650
+ expect(calls[2][3]).toBe(false) // normalized
651
+ expect(calls[2][4]).toBe(32) // stride
652
+ expect(calls[2][5]).toBe(24) // offset
653
+ })
654
+
655
+ it('should handle instanced interleaved attributes', () => {
656
+ const schema = {
657
+ instanceData: {
658
+ layout: [
659
+ { key: 'a_instancePos', kind: 'vec3' },
660
+ { key: 'a_instanceScale', kind: 'float' },
661
+ ],
662
+ instanced: true,
663
+ },
664
+ } satisfies InterleavedAttributeSchema
665
+
666
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
667
+
668
+ interleavedAttributes.instanceData.bind()
669
+
670
+ expect(gl.vertexAttribDivisor).toHaveBeenCalledTimes(2)
671
+ expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 1)
672
+ })
673
+
674
+ it('should handle integer attributes in layout', () => {
675
+ const schema = {
676
+ data: {
677
+ layout: [
678
+ { key: 'a_position', kind: 'vec2' },
679
+ { key: 'a_index', kind: 'int' },
680
+ { key: 'a_flags', kind: 'ivec2' },
681
+ ],
682
+ instanced: false,
683
+ },
684
+ } satisfies InterleavedAttributeSchema
685
+
686
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
687
+
688
+ interleavedAttributes.data.bind()
689
+
690
+ const calls = (gl.vertexAttribPointer as any).mock.calls
691
+
692
+ // Check that integer attributes use INT type
693
+ expect(calls[0][2]).toBe(gl.FLOAT) // vec2
694
+ expect(calls[1][2]).toBe(gl.INT) // int
695
+ expect(calls[2][2]).toBe(gl.INT) // ivec2
696
+ })
697
+
698
+ it('should set buffer data', () => {
699
+ const schema = {
700
+ vertexData: {
701
+ layout: [
702
+ { key: 'a_position', kind: 'vec2' },
703
+ { key: 'a_color', kind: 'vec4' },
704
+ ],
705
+ instanced: false,
706
+ },
707
+ } satisfies InterleavedAttributeSchema
708
+
709
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
710
+ const data = new Float32Array([
711
+ // vertex 0
712
+ 1,
713
+ 2, // position
714
+ 1,
715
+ 0,
716
+ 0,
717
+ 1, // color
718
+ // vertex 1
719
+ 3,
720
+ 4, // position
721
+ 0,
722
+ 1,
723
+ 0,
724
+ 1, // color
725
+ ])
726
+
727
+ interleavedAttributes.vertexData.set(data)
728
+ expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
729
+ })
730
+
731
+ it('should use VAO when available', () => {
732
+ const vertexArray = { id: 1 }
733
+ const vaoExt = {
734
+ createVertexArrayOES: vi.fn(() => vertexArray),
735
+ bindVertexArrayOES: vi.fn(),
736
+ deleteVertexArrayOES: vi.fn(),
737
+ }
738
+
739
+ // Override the getExtension method for this test
740
+ const originalGetExtension = gl.getExtension
741
+ gl.getExtension = vi.fn((name: string) => {
742
+ if (name === 'OES_vertex_array_object') return vaoExt as any
743
+ return originalGetExtension.call(gl, name)
744
+ }) as any
745
+
746
+ const schema = {
747
+ vertexData: {
748
+ layout: [{ key: 'a_position', kind: 'vec2' }],
749
+ instanced: false,
750
+ },
751
+ } satisfies InterleavedAttributeSchema
752
+
753
+ const interleavedAttributes = interleavedAttributeView(gl as any, program, schema)
754
+
755
+ expect(vaoExt.createVertexArrayOES).toHaveBeenCalled()
756
+ expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(vertexArray)
757
+ expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(null) // unbind after setup
758
+
759
+ // Test bind
760
+ interleavedAttributes.vertexData.bind()
761
+ expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(vertexArray)
762
+
763
+ // Test unbind
764
+ interleavedAttributes.vertexData.unbind()
765
+ expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(null)
766
+
767
+ // Test dispose
768
+ interleavedAttributes.vertexData.dispose()
769
+ expect(vaoExt.deleteVertexArrayOES).toHaveBeenCalledWith(vertexArray)
770
+
771
+ // Restore original method
772
+ gl.getExtension = originalGetExtension
773
+ })
774
+
775
+ it('should dispose buffers', () => {
776
+ const schema = {
777
+ data1: {
778
+ layout: [{ key: 'a_pos', kind: 'vec2' }],
779
+ instanced: false,
780
+ },
781
+ data2: {
782
+ layout: [{ key: 'a_color', kind: 'vec4' }],
783
+ instanced: false,
784
+ },
785
+ } satisfies InterleavedAttributeSchema
786
+
787
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
788
+
789
+ interleavedAttributes.data1.dispose()
790
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(1)
791
+
792
+ interleavedAttributes.data2.dispose()
793
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
794
+ })
795
+
796
+ it('should dispose all resources on abort signal', () => {
797
+ const controller = new AbortController()
798
+ const schema = {
799
+ data1: {
800
+ layout: [{ key: 'a_position', kind: 'vec2' }],
801
+ instanced: false,
802
+ },
803
+ data2: {
804
+ layout: [{ key: 'a_color', kind: 'vec4' }],
805
+ instanced: false,
806
+ },
807
+ } satisfies InterleavedAttributeSchema
808
+
809
+ interleavedAttributeView(gl, program, schema, {
810
+ signal: controller.signal,
811
+ })
812
+
813
+ controller.abort()
814
+
815
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
816
+ })
817
+
818
+ it('should throw error if attribute not found in program', () => {
819
+ gl.getAttribLocation = vi.fn(() => -1)
820
+
821
+ const schema = {
822
+ data: {
823
+ layout: [{ key: 'a_notFound', kind: 'vec2' }],
824
+ instanced: false,
825
+ },
826
+ } satisfies InterleavedAttributeSchema
827
+
828
+ expect(() => interleavedAttributeView(gl, program, schema)).toThrow(
829
+ "Attribute 'a_notFound' not found",
830
+ )
831
+ })
832
+
833
+ it('should support symbols as interleaved attribute keys', () => {
834
+ const data_symbol = Symbol('data')
835
+ const a_pos_symbol = Symbol('a_pos')
836
+ const a_col_symbol = Symbol('a_col')
837
+
838
+ const schema = {
839
+ [data_symbol]: {
840
+ layout: [
841
+ { key: a_pos_symbol, kind: 'vec3' },
842
+ { key: a_col_symbol, kind: 'vec4' },
843
+ ],
844
+ instanced: false,
845
+ },
846
+ } satisfies InterleavedAttributeSchema
847
+
848
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
849
+
850
+ expect(interleavedAttributes[data_symbol]).toBeDefined()
851
+ expect(interleavedAttributes[data_symbol].bind).toBeInstanceOf(Function)
852
+ expect(interleavedAttributes[data_symbol].unbind).toBeInstanceOf(Function)
853
+ expect(interleavedAttributes[data_symbol].dispose).toBeInstanceOf(Function)
854
+ expect(interleavedAttributes[data_symbol].set).toBeInstanceOf(Function)
855
+
856
+ // Test binding
857
+ interleavedAttributes[data_symbol].bind()
858
+
859
+ // Should bind buffer and set up attributes
860
+ expect(gl.bindBuffer).toHaveBeenCalled()
861
+ expect(gl.enableVertexAttribArray).toHaveBeenCalledTimes(2)
862
+ expect(gl.vertexAttribPointer).toHaveBeenCalledTimes(2)
863
+ })
864
+
865
+ it('should support mixed string and symbol keys in interleaved layout', () => {
866
+ const vertexData = Symbol('vertexData')
867
+ const a_normal_symbol = Symbol('a_normal')
868
+
869
+ const schema = {
870
+ [vertexData]: {
871
+ layout: [
872
+ { key: 'a_position', kind: 'vec3' },
873
+ { key: a_normal_symbol, kind: 'vec3' },
874
+ { key: 'a_uv', kind: 'vec2' },
875
+ ],
876
+ instanced: false,
877
+ },
878
+ } satisfies InterleavedAttributeSchema
879
+
880
+ const interleavedAttributes = interleavedAttributeView(gl, program, schema)
881
+
882
+ expect(interleavedAttributes[vertexData]).toBeDefined()
883
+
884
+ // Test that it correctly calculates stride
885
+ interleavedAttributes[vertexData].bind()
886
+
887
+ const calls = (gl.vertexAttribPointer as any).mock.calls
888
+ // All calls should have the same stride (3+3+2)*4 = 32 bytes
889
+ expect(calls[0][4]).toBe(32)
890
+ expect(calls[1][4]).toBe(32)
891
+ expect(calls[2][4]).toBe(32)
892
+ })
893
+ })
894
+
895
+ describe('bufferView', () => {
896
+ let gl: ReturnType<typeof createMockGL>
897
+
898
+ beforeEach(() => {
899
+ gl = createMockGL()
900
+ })
901
+
902
+ it('should create buffer methods with default options', () => {
903
+ const schema = {
904
+ vertices: {},
905
+ indices: {},
906
+ } satisfies BufferSchema
907
+
908
+ const buffers = bufferView(gl, schema)
909
+
910
+ expect(buffers.vertices).toBeDefined()
911
+ expect(buffers.vertices.bind).toBeInstanceOf(Function)
912
+ expect(buffers.vertices.dispose).toBeInstanceOf(Function)
913
+ expect(buffers.vertices.set).toBeInstanceOf(Function)
914
+ expect(gl.createBuffer).toHaveBeenCalledTimes(2)
915
+ })
916
+
917
+ it('should create buffers with specified target', () => {
918
+ const schema = {
919
+ vertices: { target: 'ARRAY_BUFFER' },
920
+ indices: { target: 'ELEMENT_ARRAY_BUFFER' },
921
+ } satisfies BufferSchema
922
+
923
+ const buffers = bufferView(gl, schema)
924
+
925
+ buffers.vertices.bind()
926
+ expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ARRAY_BUFFER, expect.any(Object))
927
+
928
+ buffers.indices.bind()
929
+ expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ELEMENT_ARRAY_BUFFER, expect.any(Object))
930
+ })
931
+
932
+ it('should set buffer data with specified usage', () => {
933
+ const schema = {
934
+ vertices: { target: 'ARRAY_BUFFER', usage: 'STATIC_DRAW' },
935
+ dynamic: { target: 'ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
936
+ } satisfies BufferSchema
937
+
938
+ const buffers = bufferView(gl, schema)
939
+ const data = new Float32Array([1, 2, 3, 4])
940
+
941
+ buffers.vertices.set(data)
942
+ expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
943
+
944
+ buffers.dynamic.set(data)
945
+ expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW)
946
+ })
947
+
948
+ it('should dispose buffers', () => {
949
+ const schema = {
950
+ buffer1: {},
951
+ buffer2: {},
952
+ } satisfies BufferSchema
953
+
954
+ const buffers = bufferView(gl, schema)
955
+
956
+ buffers.buffer1.dispose()
957
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(1)
958
+
959
+ buffers.buffer2.dispose()
960
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
961
+ })
962
+
963
+ it('should dispose all buffers on abort signal', () => {
964
+ const controller = new AbortController()
965
+ const schema = {
966
+ buffer1: {},
967
+ buffer2: {},
968
+ buffer3: {},
969
+ } satisfies BufferSchema
970
+
971
+ bufferView(gl, schema, { signal: controller.signal })
972
+
973
+ controller.abort()
974
+
975
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(3)
976
+ })
977
+
978
+ it('should support symbols as buffer keys', () => {
979
+ const vertices_symbol = Symbol('vertices')
980
+ const indices_symbol = Symbol('indices')
981
+
982
+ const schema = {
983
+ [vertices_symbol]: { target: 'ARRAY_BUFFER' },
984
+ [indices_symbol]: { target: 'ELEMENT_ARRAY_BUFFER' },
985
+ } satisfies BufferSchema
986
+
987
+ const buffers = bufferView(gl, schema)
988
+
989
+ expect(buffers[vertices_symbol]).toBeDefined()
990
+ expect(buffers[vertices_symbol].bind).toBeInstanceOf(Function)
991
+ expect(buffers[vertices_symbol].dispose).toBeInstanceOf(Function)
992
+ expect(buffers[vertices_symbol].set).toBeInstanceOf(Function)
993
+
994
+ expect(buffers[indices_symbol]).toBeDefined()
995
+
996
+ // Test binding
997
+ buffers[vertices_symbol].bind()
998
+ expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ARRAY_BUFFER, expect.any(Object))
999
+
1000
+ buffers[indices_symbol].bind()
1001
+ expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ELEMENT_ARRAY_BUFFER, expect.any(Object))
1002
+ })
1003
+
1004
+ it('should support setting data with symbol keys', () => {
1005
+ const data_symbol = Symbol('data')
1006
+
1007
+ const schema = {
1008
+ [data_symbol]: { target: 'ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
1009
+ } satisfies BufferSchema
1010
+
1011
+ const buffers = bufferView(gl, schema)
1012
+ const data = new Float32Array([1, 2, 3, 4, 5, 6])
1013
+
1014
+ buffers[data_symbol].set(data)
1015
+ expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW)
1016
+ })
1017
+ })