@bigmistqke/view.gl 0.1.1 → 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.
- package/.claude/settings.local.json +9 -0
- package/.github/ISSUE_TEMPLATE/bug-report.yml +31 -0
- package/.github/ISSUE_TEMPLATE/feature-request.yml +27 -0
- package/.github/workflows/codeql.yml +46 -0
- package/.github/workflows/deploy-page.yml +38 -0
- package/.github/workflows/format.yml +40 -0
- package/README.md +92 -35
- package/dev/404.html +19 -0
- package/dev/dist/404.html +19 -0
- package/dev/dist/assets/bezier-DuLTb2d1.js +294 -0
- package/dev/dist/assets/game_of_life-D7qHdFcv.js +202 -0
- package/dev/dist/assets/grid-Ie22U_9K.js +173 -0
- package/dev/dist/assets/index-0qrXMlfv.js +109 -0
- package/dev/dist/assets/index-C2N3u8wj.css +12 -0
- package/dev/dist/assets/main-BZ1N6pp6.js +32 -0
- package/dev/dist/assets/overview-DAxtMIw5.js +78 -0
- package/dev/dist/assets/pix_sim-DIhzM_vu.js +501 -0
- package/dev/dist/assets/symbol-BouRL-Ni.js +65 -0
- package/dev/dist/assets/tag-C_bg62fD.js +516 -0
- package/dev/dist/assets/tetris-IMnkSi9p.js +344 -0
- package/dev/dist/assets/utils-CvCgsM08.js +50 -0
- package/dev/dist/index.html +51 -0
- package/dev/index.html +39 -10
- package/dev/{bezier.ts → src/examples/bezier.ts} +1 -1
- package/dev/{grid.ts → src/examples/grid.ts} +1 -1
- package/dev/{pix_sim.ts → src/examples/pix_sim.ts} +112 -97
- package/dev/src/examples/symbol.ts +79 -0
- package/dev/{tetris.ts → src/examples/tetris.ts} +1 -0
- package/dev/src/index.css +12 -0
- package/dev/src/index.ts +69 -0
- package/dev/{utils.ts → src/utils.ts} +8 -2
- package/dev/vite.config.ts +9 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +81 -27
- package/dist/index.mjs.map +1 -1
- package/dist/tag.d.mts +9 -9
- package/dist/tag.mjs +46 -17
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-DxpS9Bae.d.mts → types-DoFAJoh-.d.mts} +18 -18
- package/package.json +20 -7
- package/src/index.ts +39 -17
- package/src/tag.ts +47 -39
- package/src/types.ts +24 -24
- package/src/{utils.ts → utils/index.ts} +39 -14
- package/src/utils/object.ts +27 -0
- package/test/index.test.ts +1017 -0
- package/test/mocks/webgl.ts +248 -0
- package/test/setup.ts +13 -0
- package/test/tag.test.ts +790 -0
- package/vitest.config.ts +15 -0
- /package/dev/{game_of_life.ts → src/examples/game_of_life.ts} +0 -0
- /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
|
+
})
|