@flighthq/scene2d-gl 0.3.0-next.906.07cea63 → 0.3.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/dist/contract.d.ts +9 -6
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +9 -6
- package/dist/contract.js.map +1 -1
- package/dist/enableGlColorAdjustmentGuards.js +2 -2
- package/dist/enableGlColorAdjustmentGuards.js.map +1 -1
- package/dist/enableGlStrokePathTessellation.d.ts +3 -0
- package/dist/enableGlStrokePathTessellation.d.ts.map +1 -0
- package/dist/enableGlStrokePathTessellation.js +8 -0
- package/dist/enableGlStrokePathTessellation.js.map +1 -0
- package/dist/explainGlScene2DCoverage.d.ts +4 -0
- package/dist/explainGlScene2DCoverage.d.ts.map +1 -0
- package/dist/explainGlScene2DCoverage.js +63 -0
- package/dist/explainGlScene2DCoverage.js.map +1 -0
- package/dist/glBitmapText.d.ts.map +1 -1
- package/dist/glBitmapText.js +20 -15
- package/dist/glBitmapText.js.map +1 -1
- package/dist/glCache.d.ts +2 -2
- package/dist/glCache.d.ts.map +1 -1
- package/dist/glCache.js +24 -92
- package/dist/glCache.js.map +1 -1
- package/dist/glClipContours.js +3 -3
- package/dist/glClipContours.js.map +1 -1
- package/dist/glClipRectangle.js +10 -8
- package/dist/glClipRectangle.js.map +1 -1
- package/dist/glColorAdjustmentMaterialFeature.d.ts.map +1 -1
- package/dist/glColorAdjustmentMaterialFeature.js +79 -76
- package/dist/glColorAdjustmentMaterialFeature.js.map +1 -1
- package/dist/glMeshShapeRenderer.d.ts +4 -0
- package/dist/glMeshShapeRenderer.d.ts.map +1 -0
- package/dist/glMeshShapeRenderer.js +89 -0
- package/dist/glMeshShapeRenderer.js.map +1 -0
- package/dist/glNode2D.js +2 -2
- package/dist/glNode2D.js.map +1 -1
- package/dist/glParticleEmitter2D.js +10 -9
- package/dist/glParticleEmitter2D.js.map +1 -1
- package/dist/glQuadBatch.d.ts.map +1 -1
- package/dist/glQuadBatch.js +18 -12
- package/dist/glQuadBatch.js.map +1 -1
- package/dist/{glSpriteBatch.d.ts → glQuadBatchWriter.d.ts} +6 -6
- package/dist/glQuadBatchWriter.d.ts.map +1 -0
- package/dist/{glSpriteBatch.js → glQuadBatchWriter.js} +84 -73
- package/dist/glQuadBatchWriter.js.map +1 -0
- package/dist/glRasterShapeRenderer.d.ts +4 -0
- package/dist/glRasterShapeRenderer.d.ts.map +1 -0
- package/dist/glRasterShapeRenderer.js +103 -0
- package/dist/glRasterShapeRenderer.js.map +1 -0
- package/dist/glRichText.js +2 -2
- package/dist/glRichText.js.map +1 -1
- package/dist/glScale9Shape.d.ts +1 -3
- package/dist/glScale9Shape.d.ts.map +1 -1
- package/dist/glScale9Shape.js +27 -29
- package/dist/glScale9Shape.js.map +1 -1
- package/dist/glShape.d.ts +2 -1
- package/dist/glShape.d.ts.map +1 -1
- package/dist/glShape.js +19 -135
- package/dist/glShape.js.map +1 -1
- package/dist/glShapeData.d.ts +7 -0
- package/dist/glShapeData.d.ts.map +1 -0
- package/dist/glShapeData.js +57 -0
- package/dist/glShapeData.js.map +1 -0
- package/dist/glShapeMesh.js +5 -5
- package/dist/glShapeMesh.js.map +1 -1
- package/dist/glShapeRasterizer.d.ts +4 -0
- package/dist/glShapeRasterizer.d.ts.map +1 -0
- package/dist/glShapeRasterizer.js +11 -0
- package/dist/glShapeRasterizer.js.map +1 -0
- package/dist/glSprite.d.ts +3 -2
- package/dist/glSprite.d.ts.map +1 -1
- package/dist/glSprite.js +64 -24
- package/dist/glSprite.js.map +1 -1
- package/dist/glStandardMaterial.d.ts +1 -1
- package/dist/glStandardMaterial.js +2 -2
- package/dist/glStandardMaterial.js.map +1 -1
- package/dist/glTestHelper.d.ts +1 -0
- package/dist/glTestHelper.d.ts.map +1 -1
- package/dist/glTestHelper.js +1 -0
- package/dist/glTestHelper.js.map +1 -1
- package/dist/glTextLabel.d.ts.map +1 -1
- package/dist/glTextLabel.js +14 -12
- package/dist/glTextLabel.js.map +1 -1
- package/dist/glTilemap.d.ts.map +1 -1
- package/dist/glTilemap.js +19 -17
- package/dist/glTilemap.js.map +1 -1
- package/dist/glVelocity.js +2 -2
- package/dist/glVelocity.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +22 -19
- package/src/enableGlColorAdjustmentGuards.test.ts +4 -4
- package/src/enableGlStrokePathTessellation.test.ts +16 -0
- package/src/explainGlScene2DCoverage.test.ts +99 -0
- package/src/glBitmapText.test.ts +18 -17
- package/src/glCache.test.ts +24 -11
- package/src/glClipContours.test.ts +57 -2
- package/src/glClipRectangle.test.ts +9 -0
- package/src/glColorAdjustmentMaterialFeature.test.ts +74 -71
- package/src/glMeshShapeRenderer.test.ts +112 -0
- package/src/glParticleEmitter2D.test.ts +38 -22
- package/src/glQuadBatch.test.ts +40 -28
- package/src/{glSpriteBatch.test.ts → glQuadBatchWriter.test.ts} +74 -53
- package/src/glRasterShapeRenderer.test.ts +109 -0
- package/src/glScale9Shape.test.ts +0 -13
- package/src/glShape.test.ts +166 -21
- package/src/glShapeData.test.ts +74 -0
- package/src/glShapeMesh.test.ts +1 -1
- package/src/glShapeRasterizer.test.ts +32 -0
- package/src/glSprite.test.ts +71 -62
- package/src/glStandardMaterial.test.ts +3 -3
- package/src/glTextLabel.test.ts +16 -16
- package/src/glTilemap.test.ts +44 -47
- package/src/glVelocity.test.ts +4 -4
- package/dist/glBitmap.d.ts +0 -4
- package/dist/glBitmap.d.ts.map +0 -1
- package/dist/glBitmap.js +0 -93
- package/dist/glBitmap.js.map +0 -1
- package/dist/glRenderTargetNode2D.d.ts +0 -11
- package/dist/glRenderTargetNode2D.d.ts.map +0 -1
- package/dist/glRenderTargetNode2D.js +0 -78
- package/dist/glRenderTargetNode2D.js.map +0 -1
- package/dist/glSpriteBatch.d.ts.map +0 -1
- package/dist/glSpriteBatch.js.map +0 -1
- package/dist/glSpriteRenderer.d.ts +0 -3
- package/dist/glSpriteRenderer.d.ts.map +0 -1
- package/dist/glSpriteRenderer.js +0 -54
- package/dist/glSpriteRenderer.js.map +0 -1
- package/dist/glVideo.d.ts +0 -6
- package/dist/glVideo.d.ts.map +0 -1
- package/dist/glVideo.js +0 -63
- package/dist/glVideo.js.map +0 -1
- package/src/glBitmap.test.ts +0 -136
- package/src/glRenderTargetNode2D.test.ts +0 -328
- package/src/glSpriteRenderer.test.ts +0 -89
- package/src/glVideo.test.ts +0 -99
|
@@ -1,29 +1,50 @@
|
|
|
1
|
-
import { createImageResource } from '@flighthq/image/contract';
|
|
2
1
|
import { getGlRenderStateRuntime } from '@flighthq/render-gl/contract';
|
|
3
|
-
import type { ColorScaleBias,
|
|
2
|
+
import type { BlendMode, ColorScaleBias, GlMaterialRenderer, GlRenderState, Material } from '@flighthq/types/contract';
|
|
4
3
|
|
|
5
4
|
import { registerGlColorAdjustmentMaterialFeature } from './glColorAdjustmentMaterialFeature';
|
|
6
5
|
import {
|
|
7
6
|
bindGlQuadBatchBaseAttributes,
|
|
8
7
|
ensureGlQuadBatchShader,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
flushGlQuadBatchWriter,
|
|
9
|
+
packGlQuadBatchMaterialInstance,
|
|
10
|
+
prepareGlQuadBatchWrite as prepareResolvedGlQuadBatchWrite,
|
|
11
|
+
recordGlQuadBatchColorScaleBias,
|
|
13
12
|
setGlQuadBatchWorldAndTexture,
|
|
14
13
|
useGlQuadBatchProgram,
|
|
15
|
-
} from './
|
|
14
|
+
} from './glQuadBatchWriter';
|
|
16
15
|
import { standardGlMaterialRenderer } from './glStandardMaterial';
|
|
17
16
|
import { createGlState } from './glTestHelper';
|
|
18
17
|
|
|
19
|
-
function makeTexture():
|
|
20
|
-
return
|
|
18
|
+
function makeTexture(): WebGLTexture {
|
|
19
|
+
return {} as WebGLTexture;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
function makeMaterial(): Material {
|
|
24
23
|
return { kind: 'TestMaterial' } as Material;
|
|
25
24
|
}
|
|
26
25
|
|
|
26
|
+
function prepareGlQuadBatchWrite(
|
|
27
|
+
state: GlRenderState,
|
|
28
|
+
texture: WebGLTexture,
|
|
29
|
+
blendMode: BlendMode | null,
|
|
30
|
+
material: Material | null,
|
|
31
|
+
materialRenderer: GlMaterialRenderer,
|
|
32
|
+
maxInstances: number,
|
|
33
|
+
smoothing: boolean | null = null,
|
|
34
|
+
): number {
|
|
35
|
+
return prepareResolvedGlQuadBatchWrite(
|
|
36
|
+
state,
|
|
37
|
+
texture,
|
|
38
|
+
false,
|
|
39
|
+
null,
|
|
40
|
+
blendMode,
|
|
41
|
+
material,
|
|
42
|
+
materialRenderer,
|
|
43
|
+
maxInstances,
|
|
44
|
+
smoothing,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
function ct(
|
|
28
49
|
redScale = 1,
|
|
29
50
|
greenScale = 1,
|
|
@@ -77,10 +98,10 @@ describe('ensureGlQuadBatchShader', () => {
|
|
|
77
98
|
});
|
|
78
99
|
});
|
|
79
100
|
|
|
80
|
-
describe('
|
|
101
|
+
describe('flushGlQuadBatchWriter', () => {
|
|
81
102
|
it('does nothing when batch count is zero', () => {
|
|
82
103
|
const { state, gl } = createGlState();
|
|
83
|
-
|
|
104
|
+
flushGlQuadBatchWriter(state);
|
|
84
105
|
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
85
106
|
});
|
|
86
107
|
|
|
@@ -89,42 +110,42 @@ describe('flushGlSpriteBatch', () => {
|
|
|
89
110
|
const runtime = getGlRenderStateRuntime(state);
|
|
90
111
|
const tex = makeTexture();
|
|
91
112
|
|
|
92
|
-
|
|
93
|
-
runtime.
|
|
94
|
-
|
|
113
|
+
prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1);
|
|
114
|
+
runtime.quadBatchWriterCount = 1;
|
|
115
|
+
flushGlQuadBatchWriter(state);
|
|
95
116
|
|
|
96
117
|
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 1);
|
|
97
|
-
expect(runtime.
|
|
98
|
-
expect(runtime.
|
|
99
|
-
expect(runtime.
|
|
100
|
-
expect(runtime.
|
|
118
|
+
expect(runtime.quadBatchWriterCount).toBe(0);
|
|
119
|
+
expect(runtime.quadBatchWriterTexture).toBeNull();
|
|
120
|
+
expect(runtime.quadBatchWriterBlendMode).toBeNull();
|
|
121
|
+
expect(runtime.quadBatchWriterMaterial).toBeNull();
|
|
101
122
|
});
|
|
102
123
|
});
|
|
103
124
|
|
|
104
|
-
describe('
|
|
125
|
+
describe('packGlQuadBatchMaterialInstance', () => {
|
|
105
126
|
it('is a no-op when no per-instance material renderer is active', () => {
|
|
106
127
|
const { state } = createGlState();
|
|
107
|
-
getGlRenderStateRuntime(state).
|
|
108
|
-
expect(() =>
|
|
128
|
+
getGlRenderStateRuntime(state).quadBatchWriterMaterialRenderer = null;
|
|
129
|
+
expect(() => packGlQuadBatchMaterialInstance(state, null, 0)).not.toThrow();
|
|
109
130
|
});
|
|
110
131
|
});
|
|
111
132
|
|
|
112
|
-
describe('
|
|
133
|
+
describe('prepareGlQuadBatchWrite', () => {
|
|
113
134
|
it('installs the pending-draw flush seam on the render state runtime', () => {
|
|
114
135
|
const { state } = createGlState();
|
|
115
136
|
const runtime = getGlRenderStateRuntime(state);
|
|
116
137
|
|
|
117
138
|
expect(runtime.flushPendingDraws).toBeNull();
|
|
118
|
-
|
|
139
|
+
prepareGlQuadBatchWrite(state, makeTexture(), null, null, standardGlMaterialRenderer, 1);
|
|
119
140
|
|
|
120
|
-
expect(runtime.flushPendingDraws).toBe(
|
|
141
|
+
expect(runtime.flushPendingDraws).toBe(flushGlQuadBatchWriter);
|
|
121
142
|
});
|
|
122
143
|
|
|
123
144
|
it('returns float index 0 for an empty batch', () => {
|
|
124
145
|
const { state } = createGlState();
|
|
125
146
|
const tex = makeTexture();
|
|
126
147
|
|
|
127
|
-
const base =
|
|
148
|
+
const base = prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 2);
|
|
128
149
|
expect(base).toBe(0);
|
|
129
150
|
});
|
|
130
151
|
|
|
@@ -134,13 +155,13 @@ describe('prepareGlSpriteBatchWrite', () => {
|
|
|
134
155
|
const tex1 = makeTexture();
|
|
135
156
|
const tex2 = makeTexture();
|
|
136
157
|
|
|
137
|
-
|
|
138
|
-
runtime.
|
|
158
|
+
prepareGlQuadBatchWrite(state, tex1, null, null, standardGlMaterialRenderer, 1);
|
|
159
|
+
runtime.quadBatchWriterCount = 1;
|
|
139
160
|
|
|
140
|
-
|
|
161
|
+
prepareGlQuadBatchWrite(state, tex2, null, null, standardGlMaterialRenderer, 1);
|
|
141
162
|
|
|
142
163
|
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
143
|
-
expect(runtime.
|
|
164
|
+
expect(runtime.quadBatchWriterTexture).toBe(tex2);
|
|
144
165
|
});
|
|
145
166
|
|
|
146
167
|
it('flushes when material changes', () => {
|
|
@@ -150,24 +171,24 @@ describe('prepareGlSpriteBatchWrite', () => {
|
|
|
150
171
|
const materialA = makeMaterial();
|
|
151
172
|
const materialB = makeMaterial();
|
|
152
173
|
|
|
153
|
-
|
|
154
|
-
runtime.
|
|
174
|
+
prepareGlQuadBatchWrite(state, tex, null, materialA, standardGlMaterialRenderer, 1);
|
|
175
|
+
runtime.quadBatchWriterCount = 1;
|
|
155
176
|
|
|
156
|
-
|
|
177
|
+
prepareGlQuadBatchWrite(state, tex, null, materialB, standardGlMaterialRenderer, 1);
|
|
157
178
|
|
|
158
179
|
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
159
|
-
expect(runtime.
|
|
180
|
+
expect(runtime.quadBatchWriterMaterial).toBe(materialB);
|
|
160
181
|
});
|
|
161
182
|
|
|
162
183
|
it('grows instance data when capacity is exceeded', () => {
|
|
163
184
|
const { state } = createGlState();
|
|
164
185
|
const runtime = getGlRenderStateRuntime(state);
|
|
165
186
|
const tex = makeTexture();
|
|
166
|
-
const initialFloats = runtime.
|
|
187
|
+
const initialFloats = runtime.quadBatchWriterInstanceData.length;
|
|
167
188
|
|
|
168
|
-
|
|
189
|
+
prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, initialFloats + 100);
|
|
169
190
|
|
|
170
|
-
expect(runtime.
|
|
191
|
+
expect(runtime.quadBatchWriterInstanceData.length).toBeGreaterThan(initialFloats);
|
|
171
192
|
});
|
|
172
193
|
|
|
173
194
|
it('flushes when per-bitmap smoothing changes on the same texture', () => {
|
|
@@ -176,13 +197,13 @@ describe('prepareGlSpriteBatchWrite', () => {
|
|
|
176
197
|
const tex = makeTexture();
|
|
177
198
|
|
|
178
199
|
// Same texture/blend/material, but a NEAREST bitmap must not share a bind with a LINEAR one.
|
|
179
|
-
|
|
180
|
-
runtime.
|
|
200
|
+
prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, false);
|
|
201
|
+
runtime.quadBatchWriterCount = 1;
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, true);
|
|
183
204
|
|
|
184
205
|
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
185
|
-
expect(runtime.
|
|
206
|
+
expect(runtime.quadBatchWriterSmoothing).toBe(true);
|
|
186
207
|
});
|
|
187
208
|
|
|
188
209
|
it('keeps same-smoothing bitmaps in one batch (no spurious flush)', () => {
|
|
@@ -190,24 +211,24 @@ describe('prepareGlSpriteBatchWrite', () => {
|
|
|
190
211
|
const runtime = getGlRenderStateRuntime(state);
|
|
191
212
|
const tex = makeTexture();
|
|
192
213
|
|
|
193
|
-
|
|
194
|
-
runtime.
|
|
195
|
-
|
|
214
|
+
prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, true);
|
|
215
|
+
runtime.quadBatchWriterCount = 1;
|
|
216
|
+
prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, true);
|
|
196
217
|
|
|
197
218
|
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
198
219
|
});
|
|
199
220
|
});
|
|
200
221
|
|
|
201
|
-
describe('
|
|
222
|
+
describe('recordGlQuadBatchColorScaleBias', () => {
|
|
202
223
|
it('skips the tint (draws untinted) and records no fold state when color adjustment is not enabled', () => {
|
|
203
224
|
const { state, gl } = createGlState();
|
|
204
225
|
const runtime = getGlRenderStateRuntime(state);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
runtime.
|
|
226
|
+
prepareGlQuadBatchWrite(state, makeTexture(), null, null, standardGlMaterialRenderer, 1);
|
|
227
|
+
recordGlQuadBatchColorScaleBias(state, ct(0.5), 0);
|
|
228
|
+
runtime.quadBatchWriterCount = 1;
|
|
208
229
|
// No fold installed → the CT mode stays uninitialized and no CT program is bound.
|
|
209
|
-
expect(runtime.
|
|
210
|
-
|
|
230
|
+
expect(runtime.quadBatchWriterColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
|
|
231
|
+
flushGlQuadBatchWriter(state);
|
|
211
232
|
expect(gl.uniform4f).not.toHaveBeenCalled();
|
|
212
233
|
expect(gl.drawElementsInstanced).toHaveBeenCalled();
|
|
213
234
|
});
|
|
@@ -215,16 +236,16 @@ describe('recordGlSpriteBatchColorScaleBias', () => {
|
|
|
215
236
|
it('is a no-op for an untinted instance whether or not the fold is enabled', () => {
|
|
216
237
|
const { state } = createGlState();
|
|
217
238
|
const runtime = getGlRenderStateRuntime(state);
|
|
218
|
-
expect(() =>
|
|
219
|
-
expect(runtime.
|
|
239
|
+
expect(() => recordGlQuadBatchColorScaleBias(state, null, 0)).not.toThrow();
|
|
240
|
+
expect(runtime.quadBatchWriterColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
|
|
220
241
|
});
|
|
221
242
|
|
|
222
243
|
it('delegates to the installed fold when color adjustment is enabled', () => {
|
|
223
244
|
const { state } = createGlState();
|
|
224
245
|
const runtime = getGlRenderStateRuntime(state);
|
|
225
246
|
registerGlColorAdjustmentMaterialFeature(state);
|
|
226
|
-
|
|
227
|
-
expect(runtime.
|
|
247
|
+
recordGlQuadBatchColorScaleBias(state, ct(0.5), 0);
|
|
248
|
+
expect(runtime.quadBatchWriterColorScaleBiasMode).toBe(CT_MODE_UNIFORM);
|
|
228
249
|
});
|
|
229
250
|
});
|
|
230
251
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type * as FlightNodeModule from '@flighthq/node/contract';
|
|
2
|
+
import { enableRenderRegistryGuards, explainRenderRegistryMisses } from '@flighthq/render/contract';
|
|
3
|
+
import { appendShapeBeginFill, appendShapeEndFill, appendShapeRectangle, createShape } from '@flighthq/shape/contract';
|
|
4
|
+
import type { RenderProxy2D } from '@flighthq/types/contract';
|
|
5
|
+
import { BatchFormat, RenderRegistry } from '@flighthq/types/contract';
|
|
6
|
+
|
|
7
|
+
import type * as GlRasterShapeModule from './glRasterShapeRenderer';
|
|
8
|
+
import { registerGlShapeRasterizer } from './glShapeRasterizer';
|
|
9
|
+
import { registerGlStandardMaterial } from './glStandardMaterial';
|
|
10
|
+
import { createGlState } from './glTestHelper';
|
|
11
|
+
import { scopeModuleMocks } from './moduleMockTestHelper';
|
|
12
|
+
|
|
13
|
+
let defaultGlRasterShapeRenderer: typeof GlRasterShapeModule.defaultGlRasterShapeRenderer;
|
|
14
|
+
let drawGlRasterShape: typeof GlRasterShapeModule.drawGlRasterShape;
|
|
15
|
+
|
|
16
|
+
scopeModuleMocks(['@flighthq/node']);
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
vi.doMock('@flighthq/node/contract', async (importOriginal) => ({
|
|
20
|
+
...(await importOriginal<typeof FlightNodeModule>()),
|
|
21
|
+
getNodeLocalBoundsRectangle: () => ({ x: 0, y: 0, width: 64, height: 48 }),
|
|
22
|
+
getNodeLocalContentRevision: (source: any) => source?.data?.version ?? 0,
|
|
23
|
+
}));
|
|
24
|
+
({ defaultGlRasterShapeRenderer, drawGlRasterShape } = await import('./glRasterShapeRenderer'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function makeShapeData() {
|
|
28
|
+
return { surface: null, lastContentId: -1, lastPixelRatio: 0, lastW: 0, lastH: 0, meshVersion: -1, meshes: null };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeShapeNode(data: Record<string, unknown>, rendererData: unknown = makeShapeData()): RenderProxy2D {
|
|
32
|
+
return {
|
|
33
|
+
source: { data: { commands: [], version: 0, ...data } },
|
|
34
|
+
rendererData,
|
|
35
|
+
blendMode: 0,
|
|
36
|
+
alpha: 1,
|
|
37
|
+
material: null,
|
|
38
|
+
materialData: null,
|
|
39
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
40
|
+
} as unknown as RenderProxy2D;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A plain solid rectangle — precisely the shape the mesh strategy would tessellate.
|
|
44
|
+
function solidShape() {
|
|
45
|
+
const shape = createShape();
|
|
46
|
+
appendShapeBeginFill(shape, 0x00cc00);
|
|
47
|
+
appendShapeRectangle(shape, 8, 8, 32, 24);
|
|
48
|
+
appendShapeEndFill(shape);
|
|
49
|
+
return shape;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('defaultGlRasterShapeRenderer', () => {
|
|
53
|
+
it('declares BatchFormat.Quad and the shared shape data lifecycle', () => {
|
|
54
|
+
expect(defaultGlRasterShapeRenderer.format).toBe(BatchFormat.Quad);
|
|
55
|
+
expect(typeof defaultGlRasterShapeRenderer.createData).toBe('function');
|
|
56
|
+
expect(typeof defaultGlRasterShapeRenderer.destroyData).toBe('function');
|
|
57
|
+
expect(defaultGlRasterShapeRenderer.submit).toBe(drawGlRasterShape);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('drawGlRasterShape', () => {
|
|
62
|
+
it('rasterizes a fill the mesh path could have tessellated, which is what pinning this strategy means', () => {
|
|
63
|
+
// The behavioural difference from defaultGlShapeRenderer: no tessellation is attempted first, so a
|
|
64
|
+
// solid rectangle still goes through the canvas replay.
|
|
65
|
+
const { state, gl } = createGlState();
|
|
66
|
+
registerGlStandardMaterial(state);
|
|
67
|
+
const rasterizer = vi.fn();
|
|
68
|
+
registerGlShapeRasterizer(state, rasterizer);
|
|
69
|
+
|
|
70
|
+
drawGlRasterShape(state, makeShapeNode({ commands: solidShape().data.commands, version: 1 }));
|
|
71
|
+
|
|
72
|
+
expect(rasterizer).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(gl.drawElements).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('replays the whole command stream, not the subset a mesh path could not express', () => {
|
|
77
|
+
// Why a rasterizing state needs the full canvas command vocabulary rather than some gap set.
|
|
78
|
+
const { state } = createGlState();
|
|
79
|
+
registerGlStandardMaterial(state);
|
|
80
|
+
const shape = solidShape();
|
|
81
|
+
let replayed: readonly unknown[] = [];
|
|
82
|
+
registerGlShapeRasterizer(state, (_ctx, commands) => {
|
|
83
|
+
replayed = commands;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
drawGlRasterShape(state, makeShapeNode({ commands: shape.data.commands, version: 1 }));
|
|
87
|
+
|
|
88
|
+
expect(replayed).toEqual(shape.data.commands);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reports a ShapeRasterizer miss when no rasterizer is registered', () => {
|
|
92
|
+
const { state } = createGlState();
|
|
93
|
+
enableRenderRegistryGuards(state);
|
|
94
|
+
drawGlRasterShape(state, makeShapeNode({ commands: solidShape().data.commands, version: 1 }));
|
|
95
|
+
expect(explainRenderRegistryMisses(state).misses).toContainEqual({
|
|
96
|
+
kind: 'Shape',
|
|
97
|
+
registry: RenderRegistry.ShapeRasterizer,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does nothing for an empty command list or absent renderer data', () => {
|
|
102
|
+
const { state } = createGlState();
|
|
103
|
+
const rasterizer = vi.fn();
|
|
104
|
+
registerGlShapeRasterizer(state, rasterizer);
|
|
105
|
+
drawGlRasterShape(state, makeShapeNode({ commands: [], version: 1 }));
|
|
106
|
+
drawGlRasterShape(state, makeShapeNode({ commands: solidShape().data.commands, version: 1 }, null));
|
|
107
|
+
expect(rasterizer).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
destroyGlScale9ShapeData,
|
|
8
8
|
drawGlScale9Shape,
|
|
9
9
|
drawGlScale9ShapeMask,
|
|
10
|
-
remapGlScale9Commands,
|
|
11
10
|
} from './glScale9Shape';
|
|
12
11
|
import { createGlState } from './glTestHelper';
|
|
13
12
|
|
|
@@ -76,15 +75,3 @@ describe('drawGlScale9ShapeMask', () => {
|
|
|
76
75
|
expect(gl.drawElements).not.toHaveBeenCalled();
|
|
77
76
|
});
|
|
78
77
|
});
|
|
79
|
-
|
|
80
|
-
describe('remapGlScale9Commands', () => {
|
|
81
|
-
it('remaps coordinates with a compatible mapper', () => {
|
|
82
|
-
const out: unknown[] = [];
|
|
83
|
-
remapGlScale9Commands(out, ['drawRectangle', 4, 10, 20, 50, 30], {
|
|
84
|
-
mapX: (x: number) => x * 2,
|
|
85
|
-
mapY: (y: number) => y * 3,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
expect(out).toEqual(['drawRectangle', 4, 20, 60, 100, 90]);
|
|
89
|
-
});
|
|
90
|
-
});
|
package/src/glShape.test.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { createImageResource } from '@flighthq/image/contract';
|
|
2
2
|
import type * as FlightNodeModule from '@flighthq/node/contract';
|
|
3
3
|
import { getGlRenderStateRuntime } from '@flighthq/render-gl/contract';
|
|
4
|
+
import {
|
|
5
|
+
appendShapeBeginFill,
|
|
6
|
+
appendShapeEndFill,
|
|
7
|
+
appendShapePath,
|
|
8
|
+
appendShapeLineStyle,
|
|
9
|
+
appendShapeLineTo,
|
|
10
|
+
appendShapeMoveTo,
|
|
11
|
+
appendShapeRectangle,
|
|
12
|
+
createShape,
|
|
13
|
+
} from '@flighthq/shape/contract';
|
|
4
14
|
import type { RenderProxy2D } from '@flighthq/types/contract';
|
|
5
|
-
import { BatchFormat } from '@flighthq/types/contract';
|
|
15
|
+
import { BatchFormat, PathCommand } from '@flighthq/types/contract';
|
|
6
16
|
|
|
17
|
+
import { enableGlStrokePathTessellation } from './enableGlStrokePathTessellation';
|
|
18
|
+
import { flushGlQuadBatchWriter } from './glQuadBatchWriter';
|
|
7
19
|
import type * as GlShapeModule from './glShape';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
20
|
+
import { registerGlShapeRasterizer } from './glShapeRasterizer';
|
|
21
|
+
import { registerGlStandardMaterial } from './glStandardMaterial';
|
|
10
22
|
import { createGlState } from './glTestHelper';
|
|
23
|
+
|
|
24
|
+
const noopRasterizer = (): void => {};
|
|
11
25
|
import { scopeModuleMocks } from './moduleMockTestHelper';
|
|
12
26
|
|
|
13
27
|
// @flighthq/node's bounds/revision queries expect a real BoundsNode; these tests drive drawGlShape
|
|
@@ -15,6 +29,7 @@ import { scopeModuleMocks } from './moduleMockTestHelper';
|
|
|
15
29
|
// this file (registry reset before the mock applies, unmock + reset after), so under a shared
|
|
16
30
|
// (isolate:false) worker it never leaks into the many real consumers of these functions (node,
|
|
17
31
|
// interaction, shape, text) — and a sibling that pre-evaluated ./glShape still picks up the stub.
|
|
32
|
+
let defaultGlMorphShapeRenderer: typeof GlShapeModule.defaultGlMorphShapeRenderer;
|
|
18
33
|
let defaultGlShapeRenderer: typeof GlShapeModule.defaultGlShapeRenderer;
|
|
19
34
|
let drawGlShape: typeof GlShapeModule.drawGlShape;
|
|
20
35
|
|
|
@@ -26,15 +41,19 @@ beforeAll(async () => {
|
|
|
26
41
|
getNodeLocalBoundsRectangle: () => ({ x: 0, y: 0, width: 64, height: 48 }),
|
|
27
42
|
getNodeLocalContentRevision: (source: any) => source?.data?.version ?? 0,
|
|
28
43
|
}));
|
|
29
|
-
({ defaultGlShapeRenderer, drawGlShape } = await import('./glShape'));
|
|
44
|
+
({ defaultGlMorphShapeRenderer, defaultGlShapeRenderer, drawGlShape } = await import('./glShape'));
|
|
30
45
|
});
|
|
31
46
|
|
|
47
|
+
// Mirrors createGlShapeData: the rasterization surface is absent until a shape actually needs one.
|
|
32
48
|
function makeShapeData() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
return {
|
|
50
|
+
surface: null,
|
|
51
|
+
lastContentId: -1,
|
|
52
|
+
lastW: 0,
|
|
53
|
+
lastH: 0,
|
|
54
|
+
meshVersion: -1,
|
|
55
|
+
meshes: null,
|
|
56
|
+
};
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
function makeShapeNode(data: Record<string, unknown> = {}, rendererData: unknown = null): RenderProxy2D {
|
|
@@ -56,6 +75,10 @@ function makeShapeNode(data: Record<string, unknown> = {}, rendererData: unknown
|
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
describe('defaultGlShapeRenderer', () => {
|
|
78
|
+
it('provides the MorphShape renderer alias', () => {
|
|
79
|
+
expect(defaultGlMorphShapeRenderer).toBe(defaultGlShapeRenderer);
|
|
80
|
+
});
|
|
81
|
+
|
|
59
82
|
it('declares BatchFormat.Quad', () => {
|
|
60
83
|
expect(defaultGlShapeRenderer.format).toBe(BatchFormat.Quad);
|
|
61
84
|
});
|
|
@@ -70,46 +93,168 @@ describe('defaultGlShapeRenderer', () => {
|
|
|
70
93
|
});
|
|
71
94
|
|
|
72
95
|
describe('drawGlShape', () => {
|
|
96
|
+
it('draws a solid fill and open solid stroke as GPU meshes in one shape', () => {
|
|
97
|
+
const { state, gl } = createGlState();
|
|
98
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
99
|
+
const shape = createShape();
|
|
100
|
+
appendShapeBeginFill(shape, 0x00cc00);
|
|
101
|
+
appendShapeLineStyle(shape, 0);
|
|
102
|
+
appendShapeRectangle(shape, 8, 8, 32, 24);
|
|
103
|
+
appendShapeEndFill(shape);
|
|
104
|
+
appendShapeLineStyle(shape, 4, 0xff0000);
|
|
105
|
+
appendShapeMoveTo(shape, 8, 4);
|
|
106
|
+
appendShapeLineTo(shape, 40, 4);
|
|
107
|
+
|
|
108
|
+
drawGlShape(state, makeShapeNode({ commands: shape.data.commands, version: 1 }, makeShapeData()));
|
|
109
|
+
|
|
110
|
+
expect(gl.drawElements).toHaveBeenCalledTimes(2);
|
|
111
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('draws a closed solid stroke ring as one GPU mesh', () => {
|
|
115
|
+
const { state, gl } = createGlState();
|
|
116
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
117
|
+
enableGlStrokePathTessellation(state);
|
|
118
|
+
const shape = createShape();
|
|
119
|
+
appendShapeLineStyle(shape, 8, 0xff0000);
|
|
120
|
+
appendShapeRectangle(shape, 8, 8, 32, 24);
|
|
121
|
+
|
|
122
|
+
drawGlShape(state, makeShapeNode({ commands: shape.data.commands, version: 1 }, makeShapeData()));
|
|
123
|
+
|
|
124
|
+
expect(gl.drawElements).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('keeps a closed stroke on the raster lane until stroke-path tessellation is enabled', () => {
|
|
129
|
+
const { state, gl } = createGlState();
|
|
130
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
131
|
+
registerGlStandardMaterial(state);
|
|
132
|
+
const shape = createShape();
|
|
133
|
+
appendShapeLineStyle(shape, 8, 0xff0000);
|
|
134
|
+
appendShapeRectangle(shape, 8, 8, 32, 24);
|
|
135
|
+
|
|
136
|
+
drawGlShape(state, makeShapeNode({ commands: shape.data.commands, version: 1 }, makeShapeData()));
|
|
137
|
+
|
|
138
|
+
expect(gl.drawElements).not.toHaveBeenCalled();
|
|
139
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('rasterizes at the state pixel ratio, so the fallback is not soft on a dense display', () => {
|
|
143
|
+
const { state } = createGlState({ pixelRatio: 3 });
|
|
144
|
+
registerGlStandardMaterial(state);
|
|
145
|
+
const rasterized: { width: number; height: number; transform: DOMMatrix }[] = [];
|
|
146
|
+
registerGlShapeRasterizer(state, (context) => {
|
|
147
|
+
rasterized.push({
|
|
148
|
+
width: context.canvas.width,
|
|
149
|
+
height: context.canvas.height,
|
|
150
|
+
transform: context.getTransform(),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
const shape = createShape();
|
|
154
|
+
appendShapeLineStyle(shape, 8, 0xff0000);
|
|
155
|
+
appendShapeRectangle(shape, 8, 8, 32, 24);
|
|
156
|
+
|
|
157
|
+
// The stubbed bounds are 64x48; a shape on the raster lane must back that with 3x the pixels and
|
|
158
|
+
// pre-scale the replay to match, or the quad samples a 1x raster stretched over a 3x screen area.
|
|
159
|
+
drawGlShape(state, makeShapeNode({ commands: shape.data.commands, version: 1 }, makeShapeData()));
|
|
160
|
+
|
|
161
|
+
expect(rasterized).toHaveLength(1);
|
|
162
|
+
expect(rasterized[0].width).toBe(192);
|
|
163
|
+
expect(rasterized[0].height).toBe(144);
|
|
164
|
+
expect(rasterized[0].transform.a).toBe(3);
|
|
165
|
+
expect(rasterized[0].transform.d).toBe(3);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('re-rasterizes when only the pixel ratio changes, since the cached raster is the wrong density', () => {
|
|
169
|
+
const { state } = createGlState({ pixelRatio: 1 });
|
|
170
|
+
registerGlStandardMaterial(state);
|
|
171
|
+
let rasterCount = 0;
|
|
172
|
+
registerGlShapeRasterizer(state, () => void rasterCount++);
|
|
173
|
+
const shape = createShape();
|
|
174
|
+
appendShapeLineStyle(shape, 8, 0xff0000);
|
|
175
|
+
appendShapeRectangle(shape, 8, 8, 32, 24);
|
|
176
|
+
const proxy = makeShapeNode({ commands: shape.data.commands, version: 1 }, makeShapeData());
|
|
177
|
+
|
|
178
|
+
drawGlShape(state, proxy);
|
|
179
|
+
drawGlShape(state, proxy);
|
|
180
|
+
expect(rasterCount).toBe(1);
|
|
181
|
+
|
|
182
|
+
(state as { pixelRatio: number }).pixelRatio = 2;
|
|
183
|
+
drawGlShape(state, proxy);
|
|
184
|
+
|
|
185
|
+
expect(rasterCount).toBe(2);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('falls back to the raster quad for a self-intersecting stroke centerline', () => {
|
|
189
|
+
const { state, gl } = createGlState();
|
|
190
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
191
|
+
enableGlStrokePathTessellation(state);
|
|
192
|
+
registerGlStandardMaterial(state);
|
|
193
|
+
const shape = createShape();
|
|
194
|
+
appendShapeLineStyle(shape, 8, 0xff0000);
|
|
195
|
+
appendShapePath(
|
|
196
|
+
shape,
|
|
197
|
+
[PathCommand.MOVE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO, PathCommand.CLOSE],
|
|
198
|
+
[8, 8, 40, 40, 8, 40, 40, 8],
|
|
199
|
+
'nonZero',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const rendererData = makeShapeData();
|
|
203
|
+
const proxy = makeShapeNode({ commands: shape.data.commands, version: 1 }, rendererData);
|
|
204
|
+
|
|
205
|
+
drawGlShape(state, proxy);
|
|
206
|
+
drawGlShape(state, proxy);
|
|
207
|
+
|
|
208
|
+
expect(gl.drawElements).not.toHaveBeenCalled();
|
|
209
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(2);
|
|
210
|
+
});
|
|
211
|
+
|
|
73
212
|
it('returns early without writing to batch when commands array is empty', () => {
|
|
74
213
|
const { state } = createGlState();
|
|
75
|
-
|
|
214
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
215
|
+
registerGlStandardMaterial(state);
|
|
76
216
|
drawGlShape(state, makeShapeNode({ commands: [] }, makeShapeData()));
|
|
77
|
-
expect(getGlRenderStateRuntime(state).
|
|
217
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(0);
|
|
78
218
|
});
|
|
79
219
|
|
|
80
220
|
it('returns early without writing to batch when rendererData is null', () => {
|
|
81
221
|
const { state } = createGlState();
|
|
82
|
-
|
|
222
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
223
|
+
registerGlStandardMaterial(state);
|
|
83
224
|
drawGlShape(state, makeShapeNode({ commands: [{}] }, null));
|
|
84
|
-
expect(getGlRenderStateRuntime(state).
|
|
225
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(0);
|
|
85
226
|
});
|
|
86
227
|
|
|
87
228
|
it('returns early without writing to batch when no material renderer is registered', () => {
|
|
88
229
|
const { state } = createGlState();
|
|
230
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
89
231
|
drawGlShape(state, makeShapeNode({ commands: [{}] }, makeShapeData()));
|
|
90
|
-
expect(getGlRenderStateRuntime(state).
|
|
232
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(0);
|
|
91
233
|
});
|
|
92
234
|
|
|
93
|
-
it('writes one instance to the
|
|
235
|
+
it('writes one instance to the quad-batch writer when shape has valid commands and bounds', () => {
|
|
94
236
|
const { state } = createGlState();
|
|
95
|
-
|
|
237
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
238
|
+
registerGlStandardMaterial(state);
|
|
96
239
|
drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
|
|
97
|
-
expect(getGlRenderStateRuntime(state).
|
|
240
|
+
expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(1);
|
|
98
241
|
});
|
|
99
242
|
|
|
100
243
|
it('draws via drawElementsInstanced after flush', () => {
|
|
101
244
|
const { state, gl } = createGlState();
|
|
102
|
-
|
|
245
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
246
|
+
registerGlStandardMaterial(state);
|
|
103
247
|
drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
|
|
104
|
-
|
|
248
|
+
flushGlQuadBatchWriter(state);
|
|
105
249
|
expect(gl.drawElementsInstanced).toHaveBeenCalled();
|
|
106
250
|
});
|
|
107
251
|
|
|
108
252
|
it('writes correct size into instance data', () => {
|
|
109
253
|
const { state } = createGlState();
|
|
110
|
-
|
|
254
|
+
registerGlShapeRasterizer(state, noopRasterizer);
|
|
255
|
+
registerGlStandardMaterial(state);
|
|
111
256
|
drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
|
|
112
|
-
const d = getGlRenderStateRuntime(state).
|
|
257
|
+
const d = getGlRenderStateRuntime(state).quadBatchWriterInstanceData;
|
|
113
258
|
expect(d[6]).toBe(64); // width from mocked bounds
|
|
114
259
|
expect(d[7]).toBe(48); // height from mocked bounds
|
|
115
260
|
});
|