@flighthq/scene2d-gl 0.2.1-next.840.857425c
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/dist/enableGlColorAdjustmentGuards.d.ts +4 -0
- package/dist/enableGlColorAdjustmentGuards.d.ts.map +1 -0
- package/dist/enableGlColorAdjustmentGuards.js +21 -0
- package/dist/enableGlColorAdjustmentGuards.js.map +1 -0
- package/dist/glBitmap.d.ts +4 -0
- package/dist/glBitmap.d.ts.map +1 -0
- package/dist/glBitmap.js +93 -0
- package/dist/glBitmap.js.map +1 -0
- package/dist/glCache.d.ts +30 -0
- package/dist/glCache.d.ts.map +1 -0
- package/dist/glCache.js +196 -0
- package/dist/glCache.js.map +1 -0
- package/dist/glClip.d.ts +3 -0
- package/dist/glClip.d.ts.map +1 -0
- package/dist/glClip.js +46 -0
- package/dist/glClip.js.map +1 -0
- package/dist/glClipContours.d.ts +4 -0
- package/dist/glClipContours.d.ts.map +1 -0
- package/dist/glClipContours.js +148 -0
- package/dist/glClipContours.js.map +1 -0
- package/dist/glClipRectangle.d.ts +4 -0
- package/dist/glClipRectangle.d.ts.map +1 -0
- package/dist/glClipRectangle.js +68 -0
- package/dist/glClipRectangle.js.map +1 -0
- package/dist/glColorAdjustment.d.ts +3 -0
- package/dist/glColorAdjustment.d.ts.map +1 -0
- package/dist/glColorAdjustment.js +361 -0
- package/dist/glColorAdjustment.js.map +1 -0
- package/dist/glDefaultMaterial.d.ts +4 -0
- package/dist/glDefaultMaterial.d.ts.map +1 -0
- package/dist/glDefaultMaterial.js +20 -0
- package/dist/glDefaultMaterial.js.map +1 -0
- package/dist/glNode2D.d.ts +5 -0
- package/dist/glNode2D.d.ts.map +1 -0
- package/dist/glNode2D.js +41 -0
- package/dist/glNode2D.js.map +1 -0
- package/dist/glNode2DRegistration.d.ts +25 -0
- package/dist/glNode2DRegistration.d.ts.map +1 -0
- package/dist/glNode2DRegistration.js +51 -0
- package/dist/glNode2DRegistration.js.map +1 -0
- package/dist/glParticleEmitter2D.d.ts +4 -0
- package/dist/glParticleEmitter2D.d.ts.map +1 -0
- package/dist/glParticleEmitter2D.js +263 -0
- package/dist/glParticleEmitter2D.js.map +1 -0
- package/dist/glQuadBatch.d.ts +3 -0
- package/dist/glQuadBatch.d.ts.map +1 -0
- package/dist/glQuadBatch.js +92 -0
- package/dist/glQuadBatch.js.map +1 -0
- package/dist/glRichText.d.ts +8 -0
- package/dist/glRichText.d.ts.map +1 -0
- package/dist/glRichText.js +161 -0
- package/dist/glRichText.js.map +1 -0
- package/dist/glScale9Mapper.d.ts +3 -0
- package/dist/glScale9Mapper.d.ts.map +1 -0
- package/dist/glScale9Mapper.js +37 -0
- package/dist/glScale9Mapper.js.map +1 -0
- package/dist/glScale9Shape.d.ts +9 -0
- package/dist/glScale9Shape.d.ts.map +1 -0
- package/dist/glScale9Shape.js +118 -0
- package/dist/glScale9Shape.js.map +1 -0
- package/dist/glShape.d.ts +4 -0
- package/dist/glShape.d.ts.map +1 -0
- package/dist/glShape.js +146 -0
- package/dist/glShape.js.map +1 -0
- package/dist/glShapeMesh.d.ts +5 -0
- package/dist/glShapeMesh.d.ts.map +1 -0
- package/dist/glShapeMesh.js +114 -0
- package/dist/glShapeMesh.js.map +1 -0
- package/dist/glSprite.d.ts +3 -0
- package/dist/glSprite.d.ts.map +1 -0
- package/dist/glSprite.js +28 -0
- package/dist/glSprite.js.map +1 -0
- package/dist/glSpriteBatch.d.ts +11 -0
- package/dist/glSpriteBatch.d.ts.map +1 -0
- package/dist/glSpriteBatch.js +277 -0
- package/dist/glSpriteBatch.js.map +1 -0
- package/dist/glSpriteRenderer.d.ts +3 -0
- package/dist/glSpriteRenderer.d.ts.map +1 -0
- package/dist/glSpriteRenderer.js +54 -0
- package/dist/glSpriteRenderer.js.map +1 -0
- package/dist/glTestHelper.d.ts +9 -0
- package/dist/glTestHelper.d.ts.map +1 -0
- package/dist/glTestHelper.js +17 -0
- package/dist/glTestHelper.js.map +1 -0
- package/dist/glTextInput.d.ts +4 -0
- package/dist/glTextInput.d.ts.map +1 -0
- package/dist/glTextInput.js +43 -0
- package/dist/glTextInput.js.map +1 -0
- package/dist/glTextLabel.d.ts +4 -0
- package/dist/glTextLabel.d.ts.map +1 -0
- package/dist/glTextLabel.js +141 -0
- package/dist/glTextLabel.js.map +1 -0
- package/dist/glTilemap.d.ts +3 -0
- package/dist/glTilemap.d.ts.map +1 -0
- package/dist/glTilemap.js +79 -0
- package/dist/glTilemap.js.map +1 -0
- package/dist/glVelocity.d.ts +22 -0
- package/dist/glVelocity.d.ts.map +1 -0
- package/dist/glVelocity.js +319 -0
- package/dist/glVelocity.js.map +1 -0
- package/dist/glVideo.d.ts +6 -0
- package/dist/glVideo.d.ts.map +1 -0
- package/dist/glVideo.js +62 -0
- package/dist/glVideo.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/moduleMockTestHelper.d.ts +15 -0
- package/dist/moduleMockTestHelper.d.ts.map +1 -0
- package/dist/moduleMockTestHelper.js +25 -0
- package/dist/moduleMockTestHelper.js.map +1 -0
- package/package.json +60 -0
- package/src/enableGlColorAdjustmentGuards.test.ts +76 -0
- package/src/glBitmap.test.ts +136 -0
- package/src/glCache.test.ts +234 -0
- package/src/glClip.test.ts +101 -0
- package/src/glClipContours.test.ts +61 -0
- package/src/glClipRectangle.test.ts +47 -0
- package/src/glColorAdjustment.test.ts +166 -0
- package/src/glDefaultMaterial.test.ts +19 -0
- package/src/glNode2D.test.ts +85 -0
- package/src/glNode2DRegistration.test.ts +132 -0
- package/src/glParticleEmitter2D.test.ts +122 -0
- package/src/glQuadBatch.test.ts +132 -0
- package/src/glRichText.test.ts +140 -0
- package/src/glScale9Mapper.test.ts +20 -0
- package/src/glScale9Shape.test.ts +90 -0
- package/src/glShape.test.ts +116 -0
- package/src/glShapeMesh.test.ts +164 -0
- package/src/glSprite.test.ts +79 -0
- package/src/glSpriteBatch.test.ts +249 -0
- package/src/glSpriteRenderer.test.ts +89 -0
- package/src/glTextInput.test.ts +51 -0
- package/src/glTextLabel.test.ts +156 -0
- package/src/glTilemap.test.ts +127 -0
- package/src/glVelocity.test.ts +156 -0
- package/src/glVideo.test.ts +98 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { addNodeChild } from '@flighthq/node';
|
|
2
|
+
import { getOrCreateRenderProxy2D, prepareScene2DRender, registerRenderer } from '@flighthq/render';
|
|
3
|
+
import { createSprite } from '@flighthq/sprite';
|
|
4
|
+
import { SpriteKind } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { renderGlSprite } from './glSprite';
|
|
7
|
+
import { createGlState } from './glTestHelper';
|
|
8
|
+
|
|
9
|
+
function makeRenderer() {
|
|
10
|
+
return { createData: () => null, submit: vi.fn() } as any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('renderGlSprite', () => {
|
|
14
|
+
it('does not throw for an empty sprite node', () => {
|
|
15
|
+
const { state } = createGlState();
|
|
16
|
+
const sprite = createSprite();
|
|
17
|
+
expect(() => {
|
|
18
|
+
prepareScene2DRender(state, sprite);
|
|
19
|
+
renderGlSprite(state, sprite);
|
|
20
|
+
}).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('calls renderer.submit for a visible sprite with a registered renderer', () => {
|
|
24
|
+
const { state } = createGlState();
|
|
25
|
+
const renderer = makeRenderer();
|
|
26
|
+
registerRenderer(state, SpriteKind, renderer);
|
|
27
|
+
|
|
28
|
+
const sprite = createSprite();
|
|
29
|
+
const data = getOrCreateRenderProxy2D(state, sprite);
|
|
30
|
+
|
|
31
|
+
prepareScene2DRender(state, sprite);
|
|
32
|
+
renderGlSprite(state, sprite);
|
|
33
|
+
|
|
34
|
+
expect(renderer.submit).toHaveBeenCalledWith(state, data);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('skips a sprite node with visible set to false', () => {
|
|
38
|
+
const { state } = createGlState();
|
|
39
|
+
const renderer = makeRenderer();
|
|
40
|
+
registerRenderer(state, SpriteKind, renderer);
|
|
41
|
+
|
|
42
|
+
const sprite = createSprite();
|
|
43
|
+
sprite.visible = false;
|
|
44
|
+
|
|
45
|
+
prepareScene2DRender(state, sprite);
|
|
46
|
+
renderGlSprite(state, sprite);
|
|
47
|
+
|
|
48
|
+
expect(renderer.submit).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('skips a sprite node with alpha at or below 0', () => {
|
|
52
|
+
const { state } = createGlState();
|
|
53
|
+
const renderer = makeRenderer();
|
|
54
|
+
registerRenderer(state, SpriteKind, renderer);
|
|
55
|
+
|
|
56
|
+
const sprite = createSprite();
|
|
57
|
+
sprite.alpha = 0;
|
|
58
|
+
|
|
59
|
+
prepareScene2DRender(state, sprite);
|
|
60
|
+
renderGlSprite(state, sprite);
|
|
61
|
+
|
|
62
|
+
expect(renderer.submit).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('recurses into children and renders visible ones', () => {
|
|
66
|
+
const { state } = createGlState();
|
|
67
|
+
const renderer = makeRenderer();
|
|
68
|
+
registerRenderer(state, SpriteKind, renderer);
|
|
69
|
+
|
|
70
|
+
const parent = createSprite();
|
|
71
|
+
const child = createSprite();
|
|
72
|
+
addNodeChild(parent, child);
|
|
73
|
+
|
|
74
|
+
prepareScene2DRender(state, parent);
|
|
75
|
+
renderGlSprite(state, parent);
|
|
76
|
+
|
|
77
|
+
expect(renderer.submit).toHaveBeenCalledTimes(2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createImageResource } from '@flighthq/image';
|
|
2
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
3
|
+
import type { ColorTransform, ImageResource, Material } from '@flighthq/types';
|
|
4
|
+
|
|
5
|
+
import { enableGlColorAdjustment } from './glColorAdjustment';
|
|
6
|
+
import { defaultGlMaterialRenderer } from './glDefaultMaterial';
|
|
7
|
+
import {
|
|
8
|
+
bindGlQuadBatchBaseAttributes,
|
|
9
|
+
ensureGlQuadBatchShader,
|
|
10
|
+
flushGlSpriteBatch,
|
|
11
|
+
packGlSpriteBatchMaterialInstance,
|
|
12
|
+
prepareGlSpriteBatchWrite,
|
|
13
|
+
recordGlSpriteBatchColorTransform,
|
|
14
|
+
setGlQuadBatchWorldAndTexture,
|
|
15
|
+
useGlQuadBatchProgram,
|
|
16
|
+
} from './glSpriteBatch';
|
|
17
|
+
import { createGlState } from './glTestHelper';
|
|
18
|
+
|
|
19
|
+
function makeTexture(): ImageResource {
|
|
20
|
+
return createImageResource(document.createElement('img'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMaterial(): Material {
|
|
24
|
+
return { kind: 'TestMaterial' } as Material;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ct(
|
|
28
|
+
redMultiplier = 1,
|
|
29
|
+
greenMultiplier = 1,
|
|
30
|
+
blueMultiplier = 1,
|
|
31
|
+
alphaMultiplier = 1,
|
|
32
|
+
redOffset = 0,
|
|
33
|
+
greenOffset = 0,
|
|
34
|
+
blueOffset = 0,
|
|
35
|
+
alphaOffset = 0,
|
|
36
|
+
): ColorTransform {
|
|
37
|
+
return {
|
|
38
|
+
redMultiplier,
|
|
39
|
+
greenMultiplier,
|
|
40
|
+
blueMultiplier,
|
|
41
|
+
alphaMultiplier,
|
|
42
|
+
redOffset,
|
|
43
|
+
greenOffset,
|
|
44
|
+
blueOffset,
|
|
45
|
+
alphaOffset,
|
|
46
|
+
} as ColorTransform;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CT_MODE_NONE = 0;
|
|
50
|
+
const CT_MODE_UNIFORM = 1;
|
|
51
|
+
|
|
52
|
+
describe('bindGlQuadBatchBaseAttributes', () => {
|
|
53
|
+
it('sets up the corner and base instance attribute pointers', () => {
|
|
54
|
+
const { state, gl } = createGlState();
|
|
55
|
+
ensureGlQuadBatchShader(state);
|
|
56
|
+
bindGlQuadBatchBaseAttributes(state, 0);
|
|
57
|
+
expect(gl.vertexAttribPointer).toHaveBeenCalled();
|
|
58
|
+
expect(gl.vertexAttribDivisor).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('ensureGlQuadBatchShader', () => {
|
|
63
|
+
it('returns a shader with a program and attribute locations', () => {
|
|
64
|
+
const { state } = createGlState();
|
|
65
|
+
const shader = ensureGlQuadBatchShader(state);
|
|
66
|
+
expect(shader.program).toBeDefined();
|
|
67
|
+
expect(typeof shader.locCorner).toBe('number');
|
|
68
|
+
expect(shader.locWorldMatrix).toBeDefined();
|
|
69
|
+
expect(shader.locTexture).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('is idempotent — returns the same shader on repeated calls', () => {
|
|
73
|
+
const { state } = createGlState();
|
|
74
|
+
const s1 = ensureGlQuadBatchShader(state);
|
|
75
|
+
const s2 = ensureGlQuadBatchShader(state);
|
|
76
|
+
expect(s1).toBe(s2);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('flushGlSpriteBatch', () => {
|
|
81
|
+
it('does nothing when batch count is zero', () => {
|
|
82
|
+
const { state, gl } = createGlState();
|
|
83
|
+
flushGlSpriteBatch(state);
|
|
84
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('draws when instances are pending and resets state', () => {
|
|
88
|
+
const { state, gl } = createGlState();
|
|
89
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
90
|
+
const tex = makeTexture();
|
|
91
|
+
|
|
92
|
+
prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 1);
|
|
93
|
+
runtime.spriteBatchCount = 1;
|
|
94
|
+
flushGlSpriteBatch(state);
|
|
95
|
+
|
|
96
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 1);
|
|
97
|
+
expect(runtime.spriteBatchCount).toBe(0);
|
|
98
|
+
expect(runtime.spriteBatchTexture).toBeNull();
|
|
99
|
+
expect(runtime.spriteBatchBlendMode).toBeNull();
|
|
100
|
+
expect(runtime.spriteBatchMaterial).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('packGlSpriteBatchMaterialInstance', () => {
|
|
105
|
+
it('is a no-op when no per-instance material renderer is active', () => {
|
|
106
|
+
const { state } = createGlState();
|
|
107
|
+
getGlRenderStateRuntime(state).spriteBatchMaterialRenderer = null;
|
|
108
|
+
expect(() => packGlSpriteBatchMaterialInstance(state, null, 0)).not.toThrow();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('prepareGlSpriteBatchWrite', () => {
|
|
113
|
+
it('returns float index 0 for an empty batch', () => {
|
|
114
|
+
const { state } = createGlState();
|
|
115
|
+
const tex = makeTexture();
|
|
116
|
+
|
|
117
|
+
const base = prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 2);
|
|
118
|
+
expect(base).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('flushes when texture changes', () => {
|
|
122
|
+
const { state, gl } = createGlState();
|
|
123
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
124
|
+
const tex1 = makeTexture();
|
|
125
|
+
const tex2 = makeTexture();
|
|
126
|
+
|
|
127
|
+
prepareGlSpriteBatchWrite(state, tex1, null, null, defaultGlMaterialRenderer, 1);
|
|
128
|
+
runtime.spriteBatchCount = 1;
|
|
129
|
+
|
|
130
|
+
prepareGlSpriteBatchWrite(state, tex2, null, null, defaultGlMaterialRenderer, 1);
|
|
131
|
+
|
|
132
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(runtime.spriteBatchTexture).toBe(tex2);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('flushes when material changes', () => {
|
|
137
|
+
const { state, gl } = createGlState();
|
|
138
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
139
|
+
const tex = makeTexture();
|
|
140
|
+
const materialA = makeMaterial();
|
|
141
|
+
const materialB = makeMaterial();
|
|
142
|
+
|
|
143
|
+
prepareGlSpriteBatchWrite(state, tex, null, materialA, defaultGlMaterialRenderer, 1);
|
|
144
|
+
runtime.spriteBatchCount = 1;
|
|
145
|
+
|
|
146
|
+
prepareGlSpriteBatchWrite(state, tex, null, materialB, defaultGlMaterialRenderer, 1);
|
|
147
|
+
|
|
148
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(runtime.spriteBatchMaterial).toBe(materialB);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('grows instance data when capacity is exceeded', () => {
|
|
153
|
+
const { state } = createGlState();
|
|
154
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
155
|
+
const tex = makeTexture();
|
|
156
|
+
const initialFloats = runtime.spriteBatchInstanceData.length;
|
|
157
|
+
|
|
158
|
+
prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, initialFloats + 100);
|
|
159
|
+
|
|
160
|
+
expect(runtime.spriteBatchInstanceData.length).toBeGreaterThan(initialFloats);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('flushes when per-bitmap smoothing changes on the same texture', () => {
|
|
164
|
+
const { state, gl } = createGlState();
|
|
165
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
166
|
+
const tex = makeTexture();
|
|
167
|
+
|
|
168
|
+
// Same texture/blend/material, but a NEAREST bitmap must not share a bind with a LINEAR one.
|
|
169
|
+
prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 1, false);
|
|
170
|
+
runtime.spriteBatchCount = 1;
|
|
171
|
+
|
|
172
|
+
prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 1, true);
|
|
173
|
+
|
|
174
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(runtime.spriteBatchSmoothing).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('keeps same-smoothing bitmaps in one batch (no spurious flush)', () => {
|
|
179
|
+
const { state, gl } = createGlState();
|
|
180
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
181
|
+
const tex = makeTexture();
|
|
182
|
+
|
|
183
|
+
prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 1, true);
|
|
184
|
+
runtime.spriteBatchCount = 1;
|
|
185
|
+
prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 1, true);
|
|
186
|
+
|
|
187
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('recordGlSpriteBatchColorTransform', () => {
|
|
192
|
+
it('skips the tint (draws untinted) and records no fold state when color adjustment is not enabled', () => {
|
|
193
|
+
const { state, gl } = createGlState();
|
|
194
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
195
|
+
prepareGlSpriteBatchWrite(state, makeTexture(), null, null, defaultGlMaterialRenderer, 1);
|
|
196
|
+
recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
|
|
197
|
+
runtime.spriteBatchCount = 1;
|
|
198
|
+
// No fold installed → the CT mode stays uninitialized and no CT program is bound.
|
|
199
|
+
expect(runtime.spriteBatchColorTransformMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
|
|
200
|
+
flushGlSpriteBatch(state);
|
|
201
|
+
expect(gl.uniform4f).not.toHaveBeenCalled();
|
|
202
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('is a no-op for an untinted instance whether or not the fold is enabled', () => {
|
|
206
|
+
const { state } = createGlState();
|
|
207
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
208
|
+
expect(() => recordGlSpriteBatchColorTransform(state, null, 0)).not.toThrow();
|
|
209
|
+
expect(runtime.spriteBatchColorTransformMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('delegates to the installed fold when color adjustment is enabled', () => {
|
|
213
|
+
const { state } = createGlState();
|
|
214
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
215
|
+
enableGlColorAdjustment(state);
|
|
216
|
+
recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
|
|
217
|
+
expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_UNIFORM);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('setGlQuadBatchWorldAndTexture', () => {
|
|
222
|
+
it('uploads the world matrix and texture unit', () => {
|
|
223
|
+
const { state, gl } = createGlState();
|
|
224
|
+
setGlQuadBatchWorldAndTexture(state, {} as WebGLUniformLocation, {} as WebGLUniformLocation);
|
|
225
|
+
expect(gl.uniformMatrix3fv).toHaveBeenCalled();
|
|
226
|
+
expect(gl.uniform1i).toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('uploads the currently bound compressed texture alpha representation', () => {
|
|
230
|
+
const { state, gl } = createGlState();
|
|
231
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
232
|
+
const straightAlphaLocation = {} as WebGLUniformLocation;
|
|
233
|
+
runtime.currentTextureStraightAlpha = true;
|
|
234
|
+
|
|
235
|
+
setGlQuadBatchWorldAndTexture(state, {} as WebGLUniformLocation, {} as WebGLUniformLocation, straightAlphaLocation);
|
|
236
|
+
|
|
237
|
+
expect(gl.uniform1i).toHaveBeenCalledWith(straightAlphaLocation, 1);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('useGlQuadBatchProgram', () => {
|
|
242
|
+
it('binds the program and records it as current', () => {
|
|
243
|
+
const { state, gl } = createGlState();
|
|
244
|
+
const program = {} as WebGLProgram;
|
|
245
|
+
useGlQuadBatchProgram(state, program);
|
|
246
|
+
expect(gl.useProgram).toHaveBeenCalledWith(program);
|
|
247
|
+
expect(getGlRenderStateRuntime(state).currentProgram).toBe(program);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
2
|
+
import type { RenderProxy2D } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { registerDefaultGlMaterial } from './glDefaultMaterial';
|
|
5
|
+
import { flushGlSpriteBatch } from './glSpriteBatch';
|
|
6
|
+
import { defaultGlSpriteRenderer } from './glSpriteRenderer';
|
|
7
|
+
import { createGlState } from './glTestHelper';
|
|
8
|
+
|
|
9
|
+
function makeAtlas(regionWidth = 32, regionHeight = 32, pivotX: number | null = null, pivotY: number | null = null) {
|
|
10
|
+
const img = document.createElement('img');
|
|
11
|
+
return {
|
|
12
|
+
image: { source: img, width: 64, height: 64 },
|
|
13
|
+
regions: [{ x: 0, y: 0, width: regionWidth, height: regionHeight, pivotX, pivotY }],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeSpriteNode(data: Record<string, unknown> = {}): RenderProxy2D {
|
|
18
|
+
return {
|
|
19
|
+
source: { data: { atlas: null, id: 0, ...data } },
|
|
20
|
+
blendMode: 0,
|
|
21
|
+
alpha: 1,
|
|
22
|
+
material: null,
|
|
23
|
+
materialData: null,
|
|
24
|
+
renderer: null,
|
|
25
|
+
traverseChildren: false,
|
|
26
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
27
|
+
} as unknown as RenderProxy2D;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('defaultGlSpriteRenderer', () => {
|
|
31
|
+
it('has a createData function', () => {
|
|
32
|
+
expect(typeof defaultGlSpriteRenderer.createData).toBe('function');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('has a submit function', () => {
|
|
36
|
+
expect(typeof defaultGlSpriteRenderer.submit).toBe('function');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('defaultGlSpriteRenderer.submit', () => {
|
|
41
|
+
it('returns early without drawing when atlas is null', () => {
|
|
42
|
+
const { state, gl } = createGlState();
|
|
43
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: null }));
|
|
44
|
+
flushGlSpriteBatch(state);
|
|
45
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns early without drawing when atlas.image is null', () => {
|
|
49
|
+
const { state, gl } = createGlState();
|
|
50
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: { image: null, regions: [] } }));
|
|
51
|
+
flushGlSpriteBatch(state);
|
|
52
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns early without drawing when id is negative', () => {
|
|
56
|
+
const { state, gl } = createGlState();
|
|
57
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: makeAtlas(), id: -1 }));
|
|
58
|
+
flushGlSpriteBatch(state);
|
|
59
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns early without drawing when region width is zero', () => {
|
|
63
|
+
const { state, gl } = createGlState();
|
|
64
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: makeAtlas(0, 32), id: 0 }));
|
|
65
|
+
flushGlSpriteBatch(state);
|
|
66
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('draws a quad when the atlas region is valid', () => {
|
|
70
|
+
const { state, gl } = createGlState();
|
|
71
|
+
registerDefaultGlMaterial(state);
|
|
72
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: makeAtlas(), id: 0 }));
|
|
73
|
+
flushGlSpriteBatch(state);
|
|
74
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('folds the region pivot through the sprite transform into batch translation', () => {
|
|
78
|
+
const { state } = createGlState();
|
|
79
|
+
registerDefaultGlMaterial(state);
|
|
80
|
+
const node = makeSpriteNode({ atlas: makeAtlas(32, 32, 7, 9), id: 0 });
|
|
81
|
+
Object.assign(node.transform2D, { a: 2, b: 3, c: 4, d: 5, tx: 100, ty: 200 });
|
|
82
|
+
|
|
83
|
+
defaultGlSpriteRenderer.submit(state, node);
|
|
84
|
+
|
|
85
|
+
const data = getGlRenderStateRuntime(state).spriteBatchInstanceData;
|
|
86
|
+
expect(data[4]).toBe(50);
|
|
87
|
+
expect(data[5]).toBe(134);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createRichText } from '@flighthq/text';
|
|
2
|
+
import { enableTextInput, setTextInputSelection } from '@flighthq/textinput';
|
|
3
|
+
import type { RenderProxy2D, RichText } from '@flighthq/types';
|
|
4
|
+
|
|
5
|
+
import { drawGlRichText } from './glRichText';
|
|
6
|
+
import { createGlState } from './glTestHelper';
|
|
7
|
+
import { drawGlTextInputOverlay, enableGlTextInput } from './glTextInput';
|
|
8
|
+
|
|
9
|
+
function makeFocusedInputProxy(): RenderProxy2D {
|
|
10
|
+
const node = createRichText({ data: { height: 40, text: 'hello', width: 100 } });
|
|
11
|
+
enableTextInput(node).focused = true;
|
|
12
|
+
return {
|
|
13
|
+
source: node,
|
|
14
|
+
blendMode: 0,
|
|
15
|
+
alpha: 1,
|
|
16
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
17
|
+
rendererData: { texture: null },
|
|
18
|
+
} as unknown as RenderProxy2D;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('drawGlTextInputOverlay', () => {
|
|
22
|
+
it('is the installed overlay function', () => {
|
|
23
|
+
expect(typeof drawGlTextInputOverlay).toBe('function');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rasterizes a focused collapsed selection without throwing', () => {
|
|
27
|
+
enableGlTextInput();
|
|
28
|
+
const { state, gl } = createGlState();
|
|
29
|
+
const renderProxy = makeFocusedInputProxy();
|
|
30
|
+
setTextInputSelection(renderProxy.source as RichText, 2, 2);
|
|
31
|
+
|
|
32
|
+
expect(() => drawGlRichText(state, renderProxy)).not.toThrow();
|
|
33
|
+
expect(gl.drawElements).toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rasterizes a focused expanded selection without throwing', () => {
|
|
37
|
+
enableGlTextInput();
|
|
38
|
+
const { state, gl } = createGlState();
|
|
39
|
+
const renderProxy = makeFocusedInputProxy();
|
|
40
|
+
setTextInputSelection(renderProxy.source as RichText, 1, 4);
|
|
41
|
+
|
|
42
|
+
expect(() => drawGlRichText(state, renderProxy)).not.toThrow();
|
|
43
|
+
expect(gl.drawElements).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('enableGlTextInput', () => {
|
|
48
|
+
it('installs the overlay without throwing', () => {
|
|
49
|
+
expect(() => enableGlTextInput()).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createImageResource } from '@flighthq/image';
|
|
2
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
3
|
+
import { createTextLabel, setTextLabelString } from '@flighthq/text';
|
|
4
|
+
import type { RenderProxy2D, TextLabel } from '@flighthq/types';
|
|
5
|
+
import { BatchFormat } from '@flighthq/types';
|
|
6
|
+
|
|
7
|
+
import { registerDefaultGlMaterial } from './glDefaultMaterial';
|
|
8
|
+
import { flushGlSpriteBatch } from './glSpriteBatch';
|
|
9
|
+
import { createGlState } from './glTestHelper';
|
|
10
|
+
import type * as GlTextLabelModule from './glTextLabel';
|
|
11
|
+
import { scopeModuleMocks } from './moduleMockTestHelper';
|
|
12
|
+
|
|
13
|
+
// @flighthq/textlayout.computeTextLayout is stubbed to emit one deterministic glyph group.
|
|
14
|
+
// scopeModuleMocks scopes the stub to this file (registry reset before the mock applies, unmock +
|
|
15
|
+
// reset after), so under a shared (isolate:false) worker it never leaks into the real text-layout
|
|
16
|
+
// consumers (text package) — and a sibling that pre-evaluated ./glTextLabel still picks up the stub.
|
|
17
|
+
let defaultGlTextLabelRenderer: typeof GlTextLabelModule.defaultGlTextLabelRenderer;
|
|
18
|
+
let drawGlTextLabel: typeof GlTextLabelModule.drawGlTextLabel;
|
|
19
|
+
|
|
20
|
+
scopeModuleMocks(['@flighthq/textlayout']);
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
vi.doMock('@flighthq/textlayout', async (importOriginal) => {
|
|
24
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
computeTextLayout: vi.fn((result: { groups: object[] }) => {
|
|
28
|
+
result.groups.push({
|
|
29
|
+
offsetX: 0,
|
|
30
|
+
offsetY: 0,
|
|
31
|
+
width: 50,
|
|
32
|
+
ascent: 12,
|
|
33
|
+
descent: 4,
|
|
34
|
+
format: {},
|
|
35
|
+
startIndex: 0,
|
|
36
|
+
endIndex: 5,
|
|
37
|
+
});
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
({ defaultGlTextLabelRenderer, drawGlTextLabel } = await import('./glTextLabel'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function makeTextData() {
|
|
45
|
+
const canvas = document.createElement('canvas');
|
|
46
|
+
canvas.width = 1;
|
|
47
|
+
canvas.height = 1;
|
|
48
|
+
const ctx = canvas.getContext('2d')!;
|
|
49
|
+
return { canvas, ctx, image: createImageResource(canvas), lastHash: '', logW: 0, logH: 0 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeTextProxy(text = '', rendererData: unknown = null): RenderProxy2D {
|
|
53
|
+
const source = createTextLabel();
|
|
54
|
+
source.data.text = text;
|
|
55
|
+
source.data.textFormat = {};
|
|
56
|
+
source.data.width = 200;
|
|
57
|
+
source.data.height = 100;
|
|
58
|
+
return {
|
|
59
|
+
source,
|
|
60
|
+
blendMode: 0,
|
|
61
|
+
alpha: 1,
|
|
62
|
+
material: null,
|
|
63
|
+
materialData: null,
|
|
64
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
65
|
+
rendererData,
|
|
66
|
+
} as unknown as RenderProxy2D;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('defaultGlTextLabelRenderer', () => {
|
|
70
|
+
it('declares BatchFormat.Quad', () => {
|
|
71
|
+
expect(defaultGlTextLabelRenderer.format).toBe(BatchFormat.Quad);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('has a createData function', () => {
|
|
75
|
+
expect(typeof defaultGlTextLabelRenderer.createData).toBe('function');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('has a submit function pointing to drawGlTextLabel', () => {
|
|
79
|
+
expect(defaultGlTextLabelRenderer.submit).toBe(drawGlTextLabel);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('drawGlTextLabel', () => {
|
|
84
|
+
it('returns early without writing to batch when text is empty', () => {
|
|
85
|
+
const { state } = createGlState();
|
|
86
|
+
registerDefaultGlMaterial(state);
|
|
87
|
+
drawGlTextLabel(state, makeTextProxy('', makeTextData()));
|
|
88
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns early without writing to batch when rendererData is null', () => {
|
|
92
|
+
const { state } = createGlState();
|
|
93
|
+
registerDefaultGlMaterial(state);
|
|
94
|
+
drawGlTextLabel(state, makeTextProxy('hello', null));
|
|
95
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns early without writing to batch when no material renderer is registered', () => {
|
|
99
|
+
const { state } = createGlState();
|
|
100
|
+
drawGlTextLabel(state, makeTextProxy('hello', makeTextData()));
|
|
101
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('writes one instance to the sprite batch when text has content', () => {
|
|
105
|
+
const { state } = createGlState();
|
|
106
|
+
registerDefaultGlMaterial(state);
|
|
107
|
+
drawGlTextLabel(state, makeTextProxy('hello', makeTextData()));
|
|
108
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('draws via drawElementsInstanced after flush', () => {
|
|
112
|
+
const { state, gl } = createGlState();
|
|
113
|
+
registerDefaultGlMaterial(state);
|
|
114
|
+
drawGlTextLabel(state, makeTextProxy('hello', makeTextData()));
|
|
115
|
+
flushGlSpriteBatch(state);
|
|
116
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('skips layout and rasterization on repeated calls when the content version is unchanged', () => {
|
|
120
|
+
const { state } = createGlState();
|
|
121
|
+
registerDefaultGlMaterial(state);
|
|
122
|
+
const data = makeTextData();
|
|
123
|
+
const proxy = makeTextProxy('hello', data);
|
|
124
|
+
drawGlTextLabel(state, proxy);
|
|
125
|
+
// Rasterization bumps the canvas resource's version (setImageResourceSource); a skipped raster leaves
|
|
126
|
+
// it untouched. First draw rasterizes (version → 1); the repeat is skipped.
|
|
127
|
+
const rasterized = data.image.version;
|
|
128
|
+
drawGlTextLabel(state, proxy);
|
|
129
|
+
expect(data.image.version).toBe(rasterized);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('re-rasterizes when the content version is bumped', () => {
|
|
133
|
+
const { state } = createGlState();
|
|
134
|
+
registerDefaultGlMaterial(state);
|
|
135
|
+
const data = makeTextData();
|
|
136
|
+
const proxy = makeTextProxy('hello', data);
|
|
137
|
+
drawGlTextLabel(state, proxy);
|
|
138
|
+
const rasterized = data.image.version;
|
|
139
|
+
setTextLabelString(proxy.source as TextLabel, 'world');
|
|
140
|
+
drawGlTextLabel(state, proxy);
|
|
141
|
+
expect(data.image.version).toBeGreaterThan(rasterized);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not re-rasterize when only alpha changes (version unchanged)', () => {
|
|
145
|
+
const { state } = createGlState();
|
|
146
|
+
registerDefaultGlMaterial(state);
|
|
147
|
+
const data = makeTextData();
|
|
148
|
+
const proxy = makeTextProxy('hello', data);
|
|
149
|
+
drawGlTextLabel(state, proxy);
|
|
150
|
+
const rasterized = data.image.version;
|
|
151
|
+
proxy.alpha = 0.5;
|
|
152
|
+
drawGlTextLabel(state, proxy);
|
|
153
|
+
// Alpha is applied per-instance in the batch; the expensive raster (and its version bump) is untouched.
|
|
154
|
+
expect(data.image.version).toBe(rasterized);
|
|
155
|
+
});
|
|
156
|
+
});
|