@bigmistqke/view.gl 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -27
- package/dev/dist/404.html +19 -0
- package/dev/dist/assets/bezier-DrA4cU_H.js +292 -0
- package/dev/dist/assets/game-of-life-BYlzWhEL.js +186 -0
- package/dev/dist/assets/glsl-composition-D_BaFFij.js +97 -0
- package/dev/dist/assets/grid-DfXHNLBm.js +181 -0
- package/dev/dist/assets/index-C2N3u8wj.css +12 -0
- package/dev/dist/assets/index-D-N0FDYs.js +110 -0
- package/dev/dist/assets/main-CSE2fki3.js +32 -0
- package/dev/dist/assets/overview-IeUhUflE.js +80 -0
- package/dev/dist/assets/pix-sim-DMJJA_WO.js +500 -0
- package/dev/dist/assets/ray-casting-DTQetDPd.js +411 -0
- package/dev/dist/assets/ray-casting-modules-Dkfw4tdc.js +765 -0
- package/dev/dist/assets/sdf-CS0uBonx.js +66 -0
- package/dev/dist/assets/symbol-BL_7GJ0M.js +67 -0
- package/dev/dist/assets/tag-C0Vskpp8.js +833 -0
- package/dev/dist/assets/tetris-D8mokodr.js +344 -0
- package/dev/dist/assets/utils-2dzuv_bW.js +62 -0
- package/dev/dist/index.html +51 -0
- package/dist/chunk-BZDGEIOT.mjs +278 -0
- package/dist/chunk-BZDGEIOT.mjs.map +1 -0
- package/dist/{chunk-EYXOSM7B.mjs → chunk-H56LJYS5.mjs} +63 -133
- package/dist/chunk-H56LJYS5.mjs.map +1 -0
- package/dist/gl.d.mts +2 -2
- package/dist/gl.mjs +1 -1
- package/dist/index.d.mts +13 -4
- package/dist/index.mjs +7 -2
- package/dist/tag.d.mts +6 -1
- package/dist/tag.mjs +19 -6
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-CyGyGi18.d.mts → types-k1Xz1iO3.d.mts} +44 -2
- package/package.json +1 -1
- package/src/gl.ts +13 -2
- package/src/index.ts +63 -39
- package/src/tag.ts +23 -2
- package/src/types.ts +43 -1
- package/src/utils.ts +45 -10
- package/test/index.test.ts +145 -29
- package/test/mocks/webgl.ts +14 -0
- package/test/tag.test.ts +41 -0
- package/dist/chunk-CJTT7WPV.mjs +0 -75
- package/dist/chunk-CJTT7WPV.mjs.map +0 -1
- package/dist/chunk-EYXOSM7B.mjs.map +0 -1
package/src/tag.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
ViewSchema,
|
|
19
19
|
ViewSchemaPartial,
|
|
20
20
|
} from './types'
|
|
21
|
-
import { createUpsertMap } from './utils'
|
|
21
|
+
import { createUpsertMap, once } from './utils'
|
|
22
22
|
|
|
23
23
|
export function glsl<TSlot extends GLSLSlot, const TSlots extends TSlot[]>(
|
|
24
24
|
template: TemplateStringsArray,
|
|
@@ -99,6 +99,11 @@ export function interleave<
|
|
|
99
99
|
export interface CompileOptions<TSchema extends ViewSchemaPartial> {
|
|
100
100
|
schema?: TSchema
|
|
101
101
|
webgl2?: boolean
|
|
102
|
+
/**
|
|
103
|
+
* Disposes what `compile` made: the program, and the buffers the view created
|
|
104
|
+
* for itself. Buffers named by the schema belong to whoever made them.
|
|
105
|
+
*/
|
|
106
|
+
signal?: AbortSignal
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export function compile<
|
|
@@ -127,6 +132,11 @@ export function compile<
|
|
|
127
132
|
..._vertex.schema.interleavedAttributes,
|
|
128
133
|
..._fragment.schema.interleavedAttributes,
|
|
129
134
|
},
|
|
135
|
+
// Never produced by a tag — a buffer has no name in the source to be
|
|
136
|
+
// parsed out of one — but `options.schema` may name buffers for the view to
|
|
137
|
+
// own, and the deep merge below indexes this object by every key it finds
|
|
138
|
+
// there. Missing, that threw.
|
|
139
|
+
buffers: {} as NonNullable<ViewSchema['buffers']>,
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
// Deep merge options.schema with generated schema
|
|
@@ -145,10 +155,21 @@ export function compile<
|
|
|
145
155
|
try {
|
|
146
156
|
const program = createProgram(gl, _vertex.template, _fragment.template)
|
|
147
157
|
|
|
158
|
+
// The same rule the views follow: `compile` made this program, so `compile`
|
|
159
|
+
// is what deletes it. A program handed in from outside would not be ours to
|
|
160
|
+
// delete, and there is no way to hand one in.
|
|
161
|
+
const resolvedView = view(gl, program, schema)
|
|
162
|
+
const dispose = once(() => {
|
|
163
|
+
resolvedView.dispose()
|
|
164
|
+
gl.deleteProgram(program)
|
|
165
|
+
})
|
|
166
|
+
options?.signal?.addEventListener('abort', dispose)
|
|
167
|
+
|
|
148
168
|
return {
|
|
149
169
|
program,
|
|
150
170
|
schema,
|
|
151
|
-
view:
|
|
171
|
+
view: resolvedView,
|
|
172
|
+
dispose,
|
|
152
173
|
vertex: _vertex.template,
|
|
153
174
|
fragment: _fragment.template,
|
|
154
175
|
} as unknown as Prettify<CompileResult<TVertex, TFragment, TSchema>>
|
package/src/types.ts
CHANGED
|
@@ -365,6 +365,15 @@ export interface AttributeMethods<T extends AttributeDefinition = AttributeDefin
|
|
|
365
365
|
*/
|
|
366
366
|
bind(): () => void
|
|
367
367
|
set(data: FormatToArray[ResolvedFormat<T>], usage?: GLUsage): { bind(): () => void }
|
|
368
|
+
/**
|
|
369
|
+
* Delete the buffer, if this attribute is the one that made it.
|
|
370
|
+
*
|
|
371
|
+
* A buffer named by the schema came from somewhere else and is deleted by
|
|
372
|
+
* whoever made it — a shared buffer is the point of passing one in, and one
|
|
373
|
+
* view cannot know whether the others are done. So this is safe to call
|
|
374
|
+
* twice and safe to call on a borrowed buffer, and it is not a way to free
|
|
375
|
+
* one: dispose the view that created it.
|
|
376
|
+
*/
|
|
368
377
|
dispose(): void
|
|
369
378
|
}
|
|
370
379
|
|
|
@@ -502,6 +511,12 @@ export interface TextureDefinition {
|
|
|
502
511
|
export interface Framebuffer {
|
|
503
512
|
texture: WebGLTexture
|
|
504
513
|
framebuffer: WebGLFramebuffer
|
|
514
|
+
/**
|
|
515
|
+
* Delete the framebuffer, and the texture only if `createFramebuffer` made
|
|
516
|
+
* it. A texture handed in belongs to whoever made it. Idempotent, and exactly
|
|
517
|
+
* what aborting the signal does.
|
|
518
|
+
*/
|
|
519
|
+
dispose(): void
|
|
505
520
|
}
|
|
506
521
|
|
|
507
522
|
/**********************************************************************************/
|
|
@@ -617,6 +632,21 @@ export interface ViewOptions {
|
|
|
617
632
|
signal?: AbortSignal
|
|
618
633
|
}
|
|
619
634
|
|
|
635
|
+
/**
|
|
636
|
+
* What a view builds for one program.
|
|
637
|
+
*
|
|
638
|
+
* A buffer declared here is OWNED by the view: it lives and dies with it, on
|
|
639
|
+
* the one signal, which is the common case — an index buffer whose lifetime is
|
|
640
|
+
* its program's has no reason to be declared apart from the attributes it is
|
|
641
|
+
* drawn with.
|
|
642
|
+
*
|
|
643
|
+
* The other two lifetimes have their own spellings, and neither is this one. A
|
|
644
|
+
* buffer that outlives the view, or is shared between views, is a standalone
|
|
645
|
+
* `bufferView` — `compile.toQuad`'s per-context quad, a buffer handed from one
|
|
646
|
+
* program's view to another's. A buffer that belongs to somebody else is NAMED
|
|
647
|
+
* by an attribute schema, and is borrowed: the view uses it and never deletes
|
|
648
|
+
* it.
|
|
649
|
+
*/
|
|
620
650
|
export interface ViewSchema {
|
|
621
651
|
uniforms?: UniformSchema
|
|
622
652
|
attributes?: AttributeSchema
|
|
@@ -641,8 +671,8 @@ export type View<T extends ViewSchema = ViewSchema> = {
|
|
|
641
671
|
interleavedAttributes: T['interleavedAttributes'] extends InterleavedAttributeSchema
|
|
642
672
|
? Prettify<InterleavedAttributeView<T['interleavedAttributes']>>
|
|
643
673
|
: {}
|
|
644
|
-
buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {}
|
|
645
674
|
uniforms: T['uniforms'] extends UniformSchema ? Prettify<UniformView<T['uniforms']>> : {}
|
|
675
|
+
buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {}
|
|
646
676
|
/**
|
|
647
677
|
* Build a vertex array from the participants one draw needs — its attributes,
|
|
648
678
|
* its interleaved layouts, its element buffer.
|
|
@@ -653,6 +683,13 @@ export type View<T extends ViewSchema = ViewSchema> = {
|
|
|
653
683
|
* something you say rather than something you inherit from call order.
|
|
654
684
|
*/
|
|
655
685
|
vao(participants: VertexArrayParticipant[]): VertexArrayMethods
|
|
686
|
+
/**
|
|
687
|
+
* Delete everything the view made: the buffers behind its attributes and
|
|
688
|
+
* layouts, and any vertex array still held from `vao()`. Buffers named by the
|
|
689
|
+
* schema are left alone — those belong to whoever made them. Idempotent, and
|
|
690
|
+
* exactly what aborting the signal does.
|
|
691
|
+
*/
|
|
692
|
+
dispose(): void
|
|
656
693
|
}
|
|
657
694
|
|
|
658
695
|
/**********************************************************************************/
|
|
@@ -881,5 +918,10 @@ export type CompileResult<
|
|
|
881
918
|
view: Prettify<View<TSchema>>
|
|
882
919
|
vertex: string
|
|
883
920
|
fragment: string
|
|
921
|
+
/**
|
|
922
|
+
* Delete the program and everything its view made. Idempotent, and
|
|
923
|
+
* exactly what aborting the signal does.
|
|
924
|
+
*/
|
|
925
|
+
dispose(): void
|
|
884
926
|
}
|
|
885
927
|
: never
|
package/src/utils.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
KIND_TO_UNIFORM_FN_NAME_MAP,
|
|
7
7
|
TextureDefinition,
|
|
8
8
|
UniformKind,
|
|
9
|
+
ViewOptions,
|
|
9
10
|
} from './types'
|
|
10
11
|
|
|
11
12
|
export function assertedNotNullish<T>(value: T, message?: string): NonNullable<T> {
|
|
@@ -13,6 +14,24 @@ export function assertedNotNullish<T>(value: T, message?: string): NonNullable<T
|
|
|
13
14
|
return value
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a teardown so it runs once.
|
|
19
|
+
*
|
|
20
|
+
* Two reasons, and both bite. A disposer is per `bind()`, and undoing twice
|
|
21
|
+
* would put back state a later `bind()` is relying on. And a `dispose()` that
|
|
22
|
+
* deletes twice invalidates a handle someone else may still be holding — the
|
|
23
|
+
* object survives while it is bound, so the fault surfaces later, somewhere
|
|
24
|
+
* else. Idempotent is the only safe shape for either.
|
|
25
|
+
*/
|
|
26
|
+
export function once(teardown: () => void): () => void {
|
|
27
|
+
let done = false
|
|
28
|
+
return () => {
|
|
29
|
+
if (done) return
|
|
30
|
+
done = true
|
|
31
|
+
teardown()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
/**********************************************************************************/
|
|
17
36
|
/* */
|
|
18
37
|
/* Kind To Uniform Name */
|
|
@@ -114,8 +133,9 @@ export function createTexture(
|
|
|
114
133
|
height,
|
|
115
134
|
}: TextureDefinition,
|
|
116
135
|
data?: ArrayBufferView | null,
|
|
136
|
+
{ signal }: ViewOptions = {},
|
|
117
137
|
): WebGLTexture {
|
|
118
|
-
const texture = gl.createTexture()
|
|
138
|
+
const texture = assertedNotNullish(gl.createTexture(), 'Failed to create texture')
|
|
119
139
|
|
|
120
140
|
function getTextureConstant(name: string) {
|
|
121
141
|
if (!(name in gl)) {
|
|
@@ -137,13 +157,17 @@ export function createTexture(
|
|
|
137
157
|
data ?? null,
|
|
138
158
|
)
|
|
139
159
|
|
|
140
|
-
console.log(minFilter)
|
|
141
160
|
// Set texture parameters
|
|
142
161
|
gl.texParameteri(gl[target], gl.TEXTURE_MIN_FILTER, gl[minFilter])
|
|
143
162
|
gl.texParameteri(gl[target], gl.TEXTURE_MAG_FILTER, gl[magFilter])
|
|
144
163
|
gl.texParameteri(gl[target], gl.TEXTURE_WRAP_S, gl[wrapS])
|
|
145
164
|
gl.texParameteri(gl[target], gl.TEXTURE_WRAP_T, gl[wrapT])
|
|
146
165
|
|
|
166
|
+
signal?.addEventListener(
|
|
167
|
+
'abort',
|
|
168
|
+
once(() => gl.deleteTexture(texture)),
|
|
169
|
+
)
|
|
170
|
+
|
|
147
171
|
return texture
|
|
148
172
|
}
|
|
149
173
|
|
|
@@ -162,7 +186,7 @@ const FRAMEBUFFER_ATTACHMENT_MAP = {
|
|
|
162
186
|
|
|
163
187
|
class FramebufferError extends Error {
|
|
164
188
|
constructor(gl: GL, status: number) {
|
|
165
|
-
let errorMessage =
|
|
189
|
+
let errorMessage = 'Framebuffer not complete. Status: '
|
|
166
190
|
switch (status) {
|
|
167
191
|
case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
|
|
168
192
|
errorMessage += 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT'
|
|
@@ -185,16 +209,15 @@ class FramebufferError extends Error {
|
|
|
185
209
|
|
|
186
210
|
export function createFramebuffer(
|
|
187
211
|
gl: GL,
|
|
188
|
-
{ attachment, texture, ...
|
|
212
|
+
{ attachment, texture: providedTexture, ...definition }: FramebufferDefinition,
|
|
213
|
+
{ signal }: ViewOptions = {},
|
|
189
214
|
) {
|
|
190
|
-
// Create texture for the framebuffer
|
|
191
|
-
|
|
215
|
+
// Create texture for the framebuffer, unless one was handed in. Which one it
|
|
216
|
+
// is decides who deletes it below — the same rule the attribute buffers follow.
|
|
217
|
+
const texture = providedTexture ?? createTexture(gl, definition)
|
|
192
218
|
|
|
193
219
|
// Create framebuffer
|
|
194
|
-
const framebuffer = assertedNotNullish(
|
|
195
|
-
gl.createFramebuffer(),
|
|
196
|
-
`Failed to create framebuffer: ${name}`,
|
|
197
|
-
)
|
|
220
|
+
const framebuffer = assertedNotNullish(gl.createFramebuffer(), 'Failed to create framebuffer')
|
|
198
221
|
|
|
199
222
|
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
|
|
200
223
|
|
|
@@ -213,9 +236,21 @@ export function createFramebuffer(
|
|
|
213
236
|
throw new FramebufferError(gl, status)
|
|
214
237
|
}
|
|
215
238
|
|
|
239
|
+
const dispose = once(() => {
|
|
240
|
+
gl.deleteFramebuffer(framebuffer)
|
|
241
|
+
// A texture handed in belongs to whoever made it — it commonly outlives the
|
|
242
|
+
// framebuffer, ping-ponged between two of them.
|
|
243
|
+
if (!providedTexture) {
|
|
244
|
+
gl.deleteTexture(texture)
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
signal?.addEventListener('abort', dispose)
|
|
249
|
+
|
|
216
250
|
return {
|
|
217
251
|
texture,
|
|
218
252
|
framebuffer,
|
|
253
|
+
dispose,
|
|
219
254
|
}
|
|
220
255
|
}
|
|
221
256
|
|
package/test/index.test.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import {
|
|
3
3
|
attributeView,
|
|
4
4
|
bufferView,
|
|
5
|
+
createFramebuffer,
|
|
6
|
+
createTexture,
|
|
5
7
|
interleavedAttributeView,
|
|
6
8
|
uniformView,
|
|
7
9
|
vaoView,
|
|
@@ -45,7 +47,6 @@ describe('view', () => {
|
|
|
45
47
|
expect(result.uniforms!.u_texture.set).toBeInstanceOf(Function)
|
|
46
48
|
expect(result.attributes).toBeUndefined()
|
|
47
49
|
expect(result.interleavedAttributes).toBeUndefined()
|
|
48
|
-
expect(result.buffers).toBeUndefined()
|
|
49
50
|
})
|
|
50
51
|
|
|
51
52
|
it('should create a view with attributes', () => {
|
|
@@ -91,23 +92,6 @@ describe('view', () => {
|
|
|
91
92
|
expect(result.interleavedAttributes!.data.buffer.set).toBeInstanceOf(Function)
|
|
92
93
|
})
|
|
93
94
|
|
|
94
|
-
it('should create a view with buffers', () => {
|
|
95
|
-
const schema = {
|
|
96
|
-
buffers: {
|
|
97
|
-
vertices: { target: 'ARRAY_BUFFER', usage: 'STATIC_DRAW' },
|
|
98
|
-
indices: { target: 'ELEMENT_ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
|
|
99
|
-
},
|
|
100
|
-
} satisfies ViewSchema
|
|
101
|
-
|
|
102
|
-
const result = view(gl, program, schema)
|
|
103
|
-
|
|
104
|
-
expect(result.buffers).toBeDefined()
|
|
105
|
-
expect(result.buffers!.vertices).toBeDefined()
|
|
106
|
-
expect(result.buffers!.vertices.bind).toBeInstanceOf(Function)
|
|
107
|
-
expect(result.buffers!.vertices.dispose).toBeInstanceOf(Function)
|
|
108
|
-
expect(result.buffers!.vertices.set).toBeInstanceOf(Function)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
95
|
it('should create a view with all components', () => {
|
|
112
96
|
const schema = {
|
|
113
97
|
uniforms: {
|
|
@@ -122,9 +106,6 @@ describe('view', () => {
|
|
|
122
106
|
instanced: false,
|
|
123
107
|
},
|
|
124
108
|
},
|
|
125
|
-
buffers: {
|
|
126
|
-
vertices: {},
|
|
127
|
-
},
|
|
128
109
|
} satisfies ViewSchema
|
|
129
110
|
|
|
130
111
|
const result = view(gl, program, schema)
|
|
@@ -132,14 +113,12 @@ describe('view', () => {
|
|
|
132
113
|
expect(result.uniforms).toBeDefined()
|
|
133
114
|
expect(result.attributes).toBeDefined()
|
|
134
115
|
expect(result.interleavedAttributes).toBeDefined()
|
|
135
|
-
expect(result.buffers).toBeDefined()
|
|
136
116
|
})
|
|
137
117
|
|
|
138
118
|
it('should create a view with symbols for all component types', () => {
|
|
139
119
|
const u_time_symbol = Symbol('u_time')
|
|
140
120
|
const a_position_symbol = Symbol('a_position')
|
|
141
121
|
const data_symbol = Symbol('data')
|
|
142
|
-
const vertices_symbol = Symbol('vertices')
|
|
143
122
|
|
|
144
123
|
const schema = {
|
|
145
124
|
uniforms: {
|
|
@@ -154,9 +133,6 @@ describe('view', () => {
|
|
|
154
133
|
instanced: false,
|
|
155
134
|
},
|
|
156
135
|
},
|
|
157
|
-
buffers: {
|
|
158
|
-
[vertices_symbol]: {},
|
|
159
|
-
},
|
|
160
136
|
} satisfies ViewSchema
|
|
161
137
|
|
|
162
138
|
const result = view(gl, program, schema)
|
|
@@ -169,9 +145,6 @@ describe('view', () => {
|
|
|
169
145
|
|
|
170
146
|
expect(result.interleavedAttributes![data_symbol]).toBeDefined()
|
|
171
147
|
expect(result.interleavedAttributes![data_symbol].buffer.bind).toBeInstanceOf(Function)
|
|
172
|
-
|
|
173
|
-
expect(result.buffers![vertices_symbol]).toBeDefined()
|
|
174
|
-
expect(result.buffers![vertices_symbol].bind).toBeInstanceOf(Function)
|
|
175
148
|
})
|
|
176
149
|
|
|
177
150
|
it('should create a view with mixed string and symbol keys', () => {
|
|
@@ -201,6 +174,94 @@ describe('view', () => {
|
|
|
201
174
|
})
|
|
202
175
|
})
|
|
203
176
|
|
|
177
|
+
describe('disposing a view', () => {
|
|
178
|
+
let gl: WebGL2RenderingContext
|
|
179
|
+
let program: WebGLProgram
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
const mock = createMockCanvas()
|
|
182
|
+
gl = mock.gl
|
|
183
|
+
program = gl.createProgram()!
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const schema = {
|
|
187
|
+
attributes: { a_position: { kind: 'vec2' } },
|
|
188
|
+
interleavedAttributes: { data: { layout: [{ key: 'a_uv', kind: 'vec2' }] } },
|
|
189
|
+
} satisfies ViewSchema
|
|
190
|
+
|
|
191
|
+
it('deletes the buffers it made, and the arrays it handed out', () => {
|
|
192
|
+
const result = view(gl, program, schema)
|
|
193
|
+
const vertexArray = result.vao([result.attributes.a_position])
|
|
194
|
+
|
|
195
|
+
result.dispose()
|
|
196
|
+
|
|
197
|
+
expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
|
|
198
|
+
expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
|
|
199
|
+
void vertexArray
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('does the same thing on abort as it does on dispose', () => {
|
|
203
|
+
const controller = new AbortController()
|
|
204
|
+
const result = view(gl, program, schema, { signal: controller.signal })
|
|
205
|
+
result.vao([result.attributes.a_position])
|
|
206
|
+
|
|
207
|
+
controller.abort()
|
|
208
|
+
const afterAbort = (gl.deleteBuffer as any).mock.calls.length
|
|
209
|
+
|
|
210
|
+
// Both routes lead to the same teardown, so taking both must not delete
|
|
211
|
+
// twice: the second delete would invalidate a handle someone else holds.
|
|
212
|
+
result.dispose()
|
|
213
|
+
expect((gl.deleteBuffer as any).mock.calls).toHaveLength(afterAbort)
|
|
214
|
+
expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('deletes a buffer the schema declared, on its own signal', () => {
|
|
218
|
+
// The regression this key exists for. It used to be built with the options
|
|
219
|
+
// dropped — `bufferView(gl, schema.buffers)` — so a view's buffers were the
|
|
220
|
+
// only resources it did not dispose, and every caller had to remember one
|
|
221
|
+
// extra line beside an abort that covered everything else.
|
|
222
|
+
const controller = new AbortController()
|
|
223
|
+
const withBuffer = {
|
|
224
|
+
attributes: { a_position: { kind: 'vec2' } },
|
|
225
|
+
buffers: { b_index: { target: 'ELEMENT_ARRAY_BUFFER' } },
|
|
226
|
+
} satisfies ViewSchema
|
|
227
|
+
const result = view(gl, program, withBuffer, { signal: controller.signal })
|
|
228
|
+
void result.buffers.b_index
|
|
229
|
+
|
|
230
|
+
controller.abort()
|
|
231
|
+
|
|
232
|
+
// The attribute's buffer and the declared one.
|
|
233
|
+
expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('leaves a buffer the schema NAMED to whoever made it', () => {
|
|
237
|
+
// Borrowed, not owned — the other half of the rule. A named buffer is the
|
|
238
|
+
// point of sharing one, and one view cannot know whether the others are
|
|
239
|
+
// done with it.
|
|
240
|
+
const borrowed = bufferView(gl, { shared: { target: 'ARRAY_BUFFER' } })
|
|
241
|
+
;(gl.deleteBuffer as any).mockClear()
|
|
242
|
+
const result = view(gl, program, {
|
|
243
|
+
attributes: { a_position: { kind: 'vec2', buffer: borrowed.shared } },
|
|
244
|
+
} satisfies ViewSchema)
|
|
245
|
+
|
|
246
|
+
result.dispose()
|
|
247
|
+
|
|
248
|
+
expect(gl.deleteBuffer).not.toHaveBeenCalled()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('forgets a vertex array the caller disposed itself', () => {
|
|
252
|
+
const result = view(gl, program, schema)
|
|
253
|
+
const vertexArray = result.vao([result.attributes.a_position])
|
|
254
|
+
|
|
255
|
+
vertexArray.dispose()
|
|
256
|
+
vertexArray.dispose()
|
|
257
|
+
result.dispose()
|
|
258
|
+
|
|
259
|
+
// Held only until disposed, so a view handing out an array per frame does
|
|
260
|
+
// not accumulate them — and disposing one twice deletes it once.
|
|
261
|
+
expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
204
265
|
describe('uniformView', () => {
|
|
205
266
|
let gl: ReturnType<typeof createMockGL>
|
|
206
267
|
let program: WebGLProgram
|
|
@@ -482,6 +543,28 @@ describe('attributeView', () => {
|
|
|
482
543
|
expect(gl.createBuffer).toHaveBeenCalledTimes(1) // Only for the custom buffer
|
|
483
544
|
})
|
|
484
545
|
|
|
546
|
+
it('does not delete a buffer it did not create', () => {
|
|
547
|
+
const customBuffer = gl.createBuffer()!
|
|
548
|
+
const controller = new AbortController()
|
|
549
|
+
const attributes = attributeView(
|
|
550
|
+
gl,
|
|
551
|
+
program,
|
|
552
|
+
{
|
|
553
|
+
a_position: { kind: 'vec2', buffer: customBuffer },
|
|
554
|
+
a_normal: { kind: 'vec2' },
|
|
555
|
+
} satisfies AttributeSchema,
|
|
556
|
+
{ signal: controller.signal },
|
|
557
|
+
)
|
|
558
|
+
const own = attributes.a_normal.buffer
|
|
559
|
+
|
|
560
|
+
controller.abort()
|
|
561
|
+
|
|
562
|
+
// A schema can name a buffer another view owns — compile.toQuad shares one
|
|
563
|
+
// across every program on a context — and disposing here must not take it.
|
|
564
|
+
expect(gl.deleteBuffer).not.toHaveBeenCalledWith(customBuffer)
|
|
565
|
+
expect(gl.deleteBuffer).toHaveBeenCalledWith(own)
|
|
566
|
+
})
|
|
567
|
+
|
|
485
568
|
it('should handle float attributes', () => {
|
|
486
569
|
const schema = {
|
|
487
570
|
a_scalar: { kind: 'float' },
|
|
@@ -1445,3 +1528,36 @@ describe('the constant source', () => {
|
|
|
1445
1528
|
expect(() => a_instance.constant).toThrow(/cannot be a constant attribute/)
|
|
1446
1529
|
})
|
|
1447
1530
|
})
|
|
1531
|
+
|
|
1532
|
+
describe('createFramebuffer', () => {
|
|
1533
|
+
let gl: ReturnType<typeof createMockGL>
|
|
1534
|
+
|
|
1535
|
+
beforeEach(() => {
|
|
1536
|
+
gl = createMockGL()
|
|
1537
|
+
gl.checkFramebufferStatus = vi.fn(() => gl.FRAMEBUFFER_COMPLETE) as any
|
|
1538
|
+
})
|
|
1539
|
+
|
|
1540
|
+
const definition = { attachment: 'color', width: 4, height: 4 } as const
|
|
1541
|
+
|
|
1542
|
+
it('deletes the texture it made', () => {
|
|
1543
|
+
const controller = new AbortController()
|
|
1544
|
+
const { texture } = createFramebuffer(gl, definition, { signal: controller.signal })
|
|
1545
|
+
|
|
1546
|
+
controller.abort()
|
|
1547
|
+
|
|
1548
|
+
expect(gl.deleteTexture).toHaveBeenCalledWith(texture)
|
|
1549
|
+
})
|
|
1550
|
+
|
|
1551
|
+
it('leaves a texture it was handed', () => {
|
|
1552
|
+
// Ping-ponged textures outlive the framebuffers pointing at them, so the
|
|
1553
|
+
// one case that matters is the one where the texture came from outside.
|
|
1554
|
+
const texture = createTexture(gl, { width: 4, height: 4 })
|
|
1555
|
+
const target = createFramebuffer(gl, { ...definition, texture })
|
|
1556
|
+
|
|
1557
|
+
target.dispose()
|
|
1558
|
+
target.dispose()
|
|
1559
|
+
|
|
1560
|
+
expect(gl.deleteFramebuffer).toHaveBeenCalledTimes(1)
|
|
1561
|
+
expect(gl.deleteTexture).not.toHaveBeenCalled()
|
|
1562
|
+
})
|
|
1563
|
+
})
|
package/test/mocks/webgl.ts
CHANGED
|
@@ -64,6 +64,20 @@ export function createMockGL() {
|
|
|
64
64
|
LINEAR: 0x2601,
|
|
65
65
|
TEXTURE_MAG_FILTER: 0x2800,
|
|
66
66
|
TEXTURE_MIN_FILTER: 0x2801,
|
|
67
|
+
TEXTURE_WRAP_S: 0x2802,
|
|
68
|
+
TEXTURE_WRAP_T: 0x2803,
|
|
69
|
+
CLAMP_TO_EDGE: 0x812f,
|
|
70
|
+
REPEAT: 0x2901,
|
|
71
|
+
RGBA: 0x1908,
|
|
72
|
+
RGB: 0x1907,
|
|
73
|
+
DEPTH_ATTACHMENT: 0x8d00,
|
|
74
|
+
STENCIL_ATTACHMENT: 0x8d20,
|
|
75
|
+
DEPTH_STENCIL_ATTACHMENT: 0x821a,
|
|
76
|
+
FRAMEBUFFER_COMPLETE: 0x8cd5,
|
|
77
|
+
FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 0x8cd6,
|
|
78
|
+
FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 0x8cd7,
|
|
79
|
+
FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 0x8cd9,
|
|
80
|
+
FRAMEBUFFER_UNSUPPORTED: 0x8cdd,
|
|
67
81
|
|
|
68
82
|
// Buffer methods
|
|
69
83
|
createBuffer: vi.fn(() => {
|
package/test/tag.test.ts
CHANGED
|
@@ -1205,6 +1205,47 @@ describe('compile', () => {
|
|
|
1205
1205
|
gl.createProgram = vi.fn(() => mockProgram)
|
|
1206
1206
|
})
|
|
1207
1207
|
|
|
1208
|
+
it('disposes the same way whether by hand or by signal', () => {
|
|
1209
|
+
const vertex = glsl`${attribute.vec2('a_position')}
|
|
1210
|
+
void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`
|
|
1211
|
+
const fragment = glsl`void main() {}`
|
|
1212
|
+
gl.deleteProgram = vi.fn()
|
|
1213
|
+
gl.deleteBuffer = vi.fn()
|
|
1214
|
+
|
|
1215
|
+
const controller = new AbortController()
|
|
1216
|
+
const result = compile(gl as WebGLRenderingContext, vertex, fragment, {
|
|
1217
|
+
signal: controller.signal,
|
|
1218
|
+
})
|
|
1219
|
+
|
|
1220
|
+
controller.abort()
|
|
1221
|
+
result.dispose()
|
|
1222
|
+
|
|
1223
|
+
// Idempotent across both routes: the program and the view's own buffer go
|
|
1224
|
+
// exactly once, however many times teardown is asked for.
|
|
1225
|
+
expect(gl.deleteProgram).toHaveBeenCalledTimes(1)
|
|
1226
|
+
expect(gl.deleteBuffer).toHaveBeenCalledTimes(1)
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
it('deletes the program it created, and nothing it borrowed', () => {
|
|
1230
|
+
const vertex = glsl`${attribute.vec2('a_position')}
|
|
1231
|
+
void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`
|
|
1232
|
+
const fragment = glsl`void main() {}`
|
|
1233
|
+
const borrowed = gl.createBuffer()!
|
|
1234
|
+
gl.deleteProgram = vi.fn()
|
|
1235
|
+
gl.deleteBuffer = vi.fn()
|
|
1236
|
+
|
|
1237
|
+
const controller = new AbortController()
|
|
1238
|
+
compile(gl as WebGLRenderingContext, vertex, fragment, {
|
|
1239
|
+
signal: controller.signal,
|
|
1240
|
+
schema: { attributes: { a_position: { buffer: borrowed } } },
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
controller.abort()
|
|
1244
|
+
|
|
1245
|
+
expect(gl.deleteProgram).toHaveBeenCalledWith(mockProgram)
|
|
1246
|
+
expect(gl.deleteBuffer).not.toHaveBeenCalledWith(borrowed)
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1208
1249
|
it('should compile shaders and return program with schema', () => {
|
|
1209
1250
|
const vertex = glsl`
|
|
1210
1251
|
${attribute.vec2('a_position')}
|
package/dist/chunk-CJTT7WPV.mjs
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// src/gl.ts
|
|
2
|
-
function isWebGL2RenderingContext(gl) {
|
|
3
|
-
return typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext;
|
|
4
|
-
}
|
|
5
|
-
function createShader(gl, type, source) {
|
|
6
|
-
const shader = gl.createShader(type);
|
|
7
|
-
if (!shader) throw new Error("Failed to create shader");
|
|
8
|
-
gl.shaderSource(shader, source);
|
|
9
|
-
gl.compileShader(shader);
|
|
10
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
11
|
-
const info = gl.getShaderInfoLog(shader);
|
|
12
|
-
gl.deleteShader(shader);
|
|
13
|
-
throw new Error(
|
|
14
|
-
`Failed to compile ${type === gl.VERTEX_SHADER ? "vertex" : "fragment"} shader: ${info}`
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
return shader;
|
|
18
|
-
}
|
|
19
|
-
function createProgram(gl, vertexSource, fragmentSource) {
|
|
20
|
-
const program = gl.createProgram();
|
|
21
|
-
if (!program) throw new Error("Failed to create WebGL program");
|
|
22
|
-
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
|
|
23
|
-
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
|
24
|
-
gl.attachShader(program, vertexShader);
|
|
25
|
-
gl.attachShader(program, fragmentShader);
|
|
26
|
-
gl.linkProgram(program);
|
|
27
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
28
|
-
const info = gl.getProgramInfoLog(program);
|
|
29
|
-
gl.deleteProgram(program);
|
|
30
|
-
gl.deleteShader(vertexShader);
|
|
31
|
-
gl.deleteShader(fragmentShader);
|
|
32
|
-
throw new Error(`Failed to link program: ${info}`);
|
|
33
|
-
}
|
|
34
|
-
gl.deleteShader(vertexShader);
|
|
35
|
-
gl.deleteShader(fragmentShader);
|
|
36
|
-
return program;
|
|
37
|
-
}
|
|
38
|
-
var INSTANCED_ARRAYS_WRAPPER_MAP = /* @__PURE__ */ new WeakMap();
|
|
39
|
-
function getInstancedArrays(gl) {
|
|
40
|
-
if (isWebGL2RenderingContext(gl)) return gl;
|
|
41
|
-
const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl);
|
|
42
|
-
if (cached) return cached;
|
|
43
|
-
const ext = gl.getExtension("ANGLE_instanced_arrays");
|
|
44
|
-
if (!ext) return void 0;
|
|
45
|
-
const wrapper = {
|
|
46
|
-
drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),
|
|
47
|
-
drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),
|
|
48
|
-
vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext)
|
|
49
|
-
};
|
|
50
|
-
INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper);
|
|
51
|
-
return wrapper;
|
|
52
|
-
}
|
|
53
|
-
var VERTEX_ARRAY_OBJECT_WRAPPER_MAP = /* @__PURE__ */ new WeakMap();
|
|
54
|
-
function getVertexArrayObject(gl) {
|
|
55
|
-
if (isWebGL2RenderingContext(gl)) return gl;
|
|
56
|
-
const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl);
|
|
57
|
-
if (cached) return cached;
|
|
58
|
-
const ext = gl.getExtension("OES_vertex_array_object");
|
|
59
|
-
if (!ext) return null;
|
|
60
|
-
const wrapper = {
|
|
61
|
-
bindVertexArray: ext.bindVertexArrayOES.bind(ext),
|
|
62
|
-
createVertexArray: ext.createVertexArrayOES.bind(ext),
|
|
63
|
-
deleteVertexArray: ext.deleteVertexArrayOES.bind(ext)
|
|
64
|
-
};
|
|
65
|
-
VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper);
|
|
66
|
-
return wrapper;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export {
|
|
70
|
-
createShader,
|
|
71
|
-
createProgram,
|
|
72
|
-
getInstancedArrays,
|
|
73
|
-
getVertexArrayObject
|
|
74
|
-
};
|
|
75
|
-
//# sourceMappingURL=chunk-CJTT7WPV.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gl.ts"],"sourcesContent":["import { RemoveSuffix } from './type-utils'\nimport type { GL } from './types'\n\nfunction isWebGL2RenderingContext(\n gl: WebGLRenderingContext | WebGL2RenderingContext,\n): gl is WebGL2RenderingContext {\n return typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext\n}\n\n/**********************************************************************************/\n/* */\n/* Create Shader */\n/* */\n/**********************************************************************************/\n\nexport function createShader(gl: GL, type: number, source: string): WebGLShader {\n const shader = gl.createShader(type)\n if (!shader) throw new Error('Failed to create shader')\n\n gl.shaderSource(shader, source)\n gl.compileShader(shader)\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const info = gl.getShaderInfoLog(shader)\n gl.deleteShader(shader)\n throw new Error(\n `Failed to compile ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader: ${info}`,\n )\n }\n\n return shader\n}\n\n/**********************************************************************************/\n/* */\n/* Create Program */\n/* */\n/**********************************************************************************/\n\nexport function createProgram(gl: GL, vertexSource: string, fragmentSource: string): WebGLProgram {\n const program = gl.createProgram()\n if (!program) throw new Error('Failed to create WebGL program')\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)\n\n gl.attachShader(program, vertexShader)\n gl.attachShader(program, fragmentShader)\n gl.linkProgram(program)\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const info = gl.getProgramInfoLog(program)\n gl.deleteProgram(program)\n gl.deleteShader(vertexShader)\n gl.deleteShader(fragmentShader)\n throw new Error(`Failed to link program: ${info}`)\n }\n\n // Clean up shaders after linking\n gl.deleteShader(vertexShader)\n gl.deleteShader(fragmentShader)\n\n return program\n}\n\n/**********************************************************************************/\n/* */\n/* Get Instanced Arrays */\n/* */\n/**********************************************************************************/\n\nconst INSTANCED_ARRAYS_WRAPPER_MAP = new WeakMap<\n GL,\n RemoveSuffix<\n Pick<\n ANGLE_instanced_arrays,\n 'drawArraysInstancedANGLE' | 'drawElementsInstancedANGLE' | 'vertexAttribDivisorANGLE'\n >,\n 'ANGLE'\n > | null\n>()\nexport function getInstancedArrays(gl: GL) {\n if (isWebGL2RenderingContext(gl)) return gl\n\n const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl)\n if (cached) return cached\n\n const ext = gl.getExtension('ANGLE_instanced_arrays')\n if (!ext) return undefined\n\n const wrapper = {\n drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),\n drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),\n vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext),\n }\n INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper)\n\n return wrapper\n}\n\n/**********************************************************************************/\n/* */\n/* Get Vertex Array Object */\n/* */\n/**********************************************************************************/\n\nconst VERTEX_ARRAY_OBJECT_WRAPPER_MAP = new WeakMap<\n GL,\n RemoveSuffix<\n Pick<\n OES_vertex_array_object,\n 'bindVertexArrayOES' | 'createVertexArrayOES' | 'deleteVertexArrayOES'\n >,\n 'OES'\n > | null\n>()\nexport function getVertexArrayObject(gl: GL) {\n if (isWebGL2RenderingContext(gl)) return gl\n\n const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl)\n if (cached) return cached\n\n const ext = gl.getExtension('OES_vertex_array_object')\n if (!ext) return null\n\n const wrapper = {\n bindVertexArray: ext.bindVertexArrayOES.bind(ext),\n createVertexArray: ext.createVertexArrayOES.bind(ext),\n deleteVertexArray: ext.deleteVertexArrayOES.bind(ext),\n }\n VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper)\n\n return wrapper\n}\n"],"mappings":";AAGA,SAAS,yBACP,IAC8B;AAC9B,SAAO,OAAO,2BAA2B,eAAe,cAAc;AACxE;AAQO,SAAS,aAAa,IAAQ,MAAc,QAA6B;AAC9E,QAAM,SAAS,GAAG,aAAa,IAAI;AACnC,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAEtD,KAAG,aAAa,QAAQ,MAAM;AAC9B,KAAG,cAAc,MAAM;AAEvB,MAAI,CAAC,GAAG,mBAAmB,QAAQ,GAAG,cAAc,GAAG;AACrD,UAAM,OAAO,GAAG,iBAAiB,MAAM;AACvC,OAAG,aAAa,MAAM;AACtB,UAAM,IAAI;AAAA,MACR,qBAAqB,SAAS,GAAG,gBAAgB,WAAW,UAAU,YAAY,IAAI;AAAA,IACxF;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,cAAc,IAAQ,cAAsB,gBAAsC;AAChG,QAAM,UAAU,GAAG,cAAc;AACjC,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,gCAAgC;AAE9D,QAAM,eAAe,aAAa,IAAI,GAAG,eAAe,YAAY;AACpE,QAAM,iBAAiB,aAAa,IAAI,GAAG,iBAAiB,cAAc;AAE1E,KAAG,aAAa,SAAS,YAAY;AACrC,KAAG,aAAa,SAAS,cAAc;AACvC,KAAG,YAAY,OAAO;AAEtB,MAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG,WAAW,GAAG;AACpD,UAAM,OAAO,GAAG,kBAAkB,OAAO;AACzC,OAAG,cAAc,OAAO;AACxB,OAAG,aAAa,YAAY;AAC5B,OAAG,aAAa,cAAc;AAC9B,UAAM,IAAI,MAAM,2BAA2B,IAAI,EAAE;AAAA,EACnD;AAGA,KAAG,aAAa,YAAY;AAC5B,KAAG,aAAa,cAAc;AAE9B,SAAO;AACT;AAQA,IAAM,+BAA+B,oBAAI,QASvC;AACK,SAAS,mBAAmB,IAAQ;AACzC,MAAI,yBAAyB,EAAE,EAAG,QAAO;AAEzC,QAAM,SAAS,6BAA6B,IAAI,EAAE;AAClD,MAAI,OAAQ,QAAO;AAEnB,QAAM,MAAM,GAAG,aAAa,wBAAwB;AACpD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU;AAAA,IACd,qBAAqB,IAAI,yBAAyB,KAAK,GAAG;AAAA,IAC1D,uBAAuB,IAAI,2BAA2B,KAAK,GAAG;AAAA,IAC9D,qBAAqB,IAAI,yBAAyB,KAAK,GAAG;AAAA,EAC5D;AACA,+BAA6B,IAAI,IAAI,OAAO;AAE5C,SAAO;AACT;AAQA,IAAM,kCAAkC,oBAAI,QAS1C;AACK,SAAS,qBAAqB,IAAQ;AAC3C,MAAI,yBAAyB,EAAE,EAAG,QAAO;AAEzC,QAAM,SAAS,gCAAgC,IAAI,EAAE;AACrD,MAAI,OAAQ,QAAO;AAEnB,QAAM,MAAM,GAAG,aAAa,yBAAyB;AACrD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU;AAAA,IACd,iBAAiB,IAAI,mBAAmB,KAAK,GAAG;AAAA,IAChD,mBAAmB,IAAI,qBAAqB,KAAK,GAAG;AAAA,IACpD,mBAAmB,IAAI,qBAAqB,KAAK,GAAG;AAAA,EACtD;AACA,kCAAgC,IAAI,IAAI,OAAO;AAE/C,SAAO;AACT;","names":[]}
|