@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.
Files changed (136) hide show
  1. package/dist/contract.d.ts +9 -6
  2. package/dist/contract.d.ts.map +1 -1
  3. package/dist/contract.js +9 -6
  4. package/dist/contract.js.map +1 -1
  5. package/dist/enableGlColorAdjustmentGuards.js +2 -2
  6. package/dist/enableGlColorAdjustmentGuards.js.map +1 -1
  7. package/dist/enableGlStrokePathTessellation.d.ts +3 -0
  8. package/dist/enableGlStrokePathTessellation.d.ts.map +1 -0
  9. package/dist/enableGlStrokePathTessellation.js +8 -0
  10. package/dist/enableGlStrokePathTessellation.js.map +1 -0
  11. package/dist/explainGlScene2DCoverage.d.ts +4 -0
  12. package/dist/explainGlScene2DCoverage.d.ts.map +1 -0
  13. package/dist/explainGlScene2DCoverage.js +63 -0
  14. package/dist/explainGlScene2DCoverage.js.map +1 -0
  15. package/dist/glBitmapText.d.ts.map +1 -1
  16. package/dist/glBitmapText.js +20 -15
  17. package/dist/glBitmapText.js.map +1 -1
  18. package/dist/glCache.d.ts +2 -2
  19. package/dist/glCache.d.ts.map +1 -1
  20. package/dist/glCache.js +24 -92
  21. package/dist/glCache.js.map +1 -1
  22. package/dist/glClipContours.js +3 -3
  23. package/dist/glClipContours.js.map +1 -1
  24. package/dist/glClipRectangle.js +10 -8
  25. package/dist/glClipRectangle.js.map +1 -1
  26. package/dist/glColorAdjustmentMaterialFeature.d.ts.map +1 -1
  27. package/dist/glColorAdjustmentMaterialFeature.js +79 -76
  28. package/dist/glColorAdjustmentMaterialFeature.js.map +1 -1
  29. package/dist/glMeshShapeRenderer.d.ts +4 -0
  30. package/dist/glMeshShapeRenderer.d.ts.map +1 -0
  31. package/dist/glMeshShapeRenderer.js +89 -0
  32. package/dist/glMeshShapeRenderer.js.map +1 -0
  33. package/dist/glNode2D.js +2 -2
  34. package/dist/glNode2D.js.map +1 -1
  35. package/dist/glParticleEmitter2D.js +10 -9
  36. package/dist/glParticleEmitter2D.js.map +1 -1
  37. package/dist/glQuadBatch.d.ts.map +1 -1
  38. package/dist/glQuadBatch.js +18 -12
  39. package/dist/glQuadBatch.js.map +1 -1
  40. package/dist/{glSpriteBatch.d.ts → glQuadBatchWriter.d.ts} +6 -6
  41. package/dist/glQuadBatchWriter.d.ts.map +1 -0
  42. package/dist/{glSpriteBatch.js → glQuadBatchWriter.js} +84 -73
  43. package/dist/glQuadBatchWriter.js.map +1 -0
  44. package/dist/glRasterShapeRenderer.d.ts +4 -0
  45. package/dist/glRasterShapeRenderer.d.ts.map +1 -0
  46. package/dist/glRasterShapeRenderer.js +103 -0
  47. package/dist/glRasterShapeRenderer.js.map +1 -0
  48. package/dist/glRichText.js +2 -2
  49. package/dist/glRichText.js.map +1 -1
  50. package/dist/glScale9Shape.d.ts +1 -3
  51. package/dist/glScale9Shape.d.ts.map +1 -1
  52. package/dist/glScale9Shape.js +27 -29
  53. package/dist/glScale9Shape.js.map +1 -1
  54. package/dist/glShape.d.ts +2 -1
  55. package/dist/glShape.d.ts.map +1 -1
  56. package/dist/glShape.js +19 -135
  57. package/dist/glShape.js.map +1 -1
  58. package/dist/glShapeData.d.ts +7 -0
  59. package/dist/glShapeData.d.ts.map +1 -0
  60. package/dist/glShapeData.js +57 -0
  61. package/dist/glShapeData.js.map +1 -0
  62. package/dist/glShapeMesh.js +5 -5
  63. package/dist/glShapeMesh.js.map +1 -1
  64. package/dist/glShapeRasterizer.d.ts +4 -0
  65. package/dist/glShapeRasterizer.d.ts.map +1 -0
  66. package/dist/glShapeRasterizer.js +11 -0
  67. package/dist/glShapeRasterizer.js.map +1 -0
  68. package/dist/glSprite.d.ts +3 -2
  69. package/dist/glSprite.d.ts.map +1 -1
  70. package/dist/glSprite.js +64 -24
  71. package/dist/glSprite.js.map +1 -1
  72. package/dist/glStandardMaterial.d.ts +1 -1
  73. package/dist/glStandardMaterial.js +2 -2
  74. package/dist/glStandardMaterial.js.map +1 -1
  75. package/dist/glTestHelper.d.ts +1 -0
  76. package/dist/glTestHelper.d.ts.map +1 -1
  77. package/dist/glTestHelper.js +1 -0
  78. package/dist/glTestHelper.js.map +1 -1
  79. package/dist/glTextLabel.d.ts.map +1 -1
  80. package/dist/glTextLabel.js +14 -12
  81. package/dist/glTextLabel.js.map +1 -1
  82. package/dist/glTilemap.d.ts.map +1 -1
  83. package/dist/glTilemap.js +19 -17
  84. package/dist/glTilemap.js.map +1 -1
  85. package/dist/glVelocity.js +2 -2
  86. package/dist/glVelocity.js.map +1 -1
  87. package/dist/index.d.ts +2 -1
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2 -1
  90. package/dist/index.js.map +1 -1
  91. package/package.json +22 -19
  92. package/src/enableGlColorAdjustmentGuards.test.ts +4 -4
  93. package/src/enableGlStrokePathTessellation.test.ts +16 -0
  94. package/src/explainGlScene2DCoverage.test.ts +99 -0
  95. package/src/glBitmapText.test.ts +18 -17
  96. package/src/glCache.test.ts +24 -11
  97. package/src/glClipContours.test.ts +57 -2
  98. package/src/glClipRectangle.test.ts +9 -0
  99. package/src/glColorAdjustmentMaterialFeature.test.ts +74 -71
  100. package/src/glMeshShapeRenderer.test.ts +112 -0
  101. package/src/glParticleEmitter2D.test.ts +38 -22
  102. package/src/glQuadBatch.test.ts +40 -28
  103. package/src/{glSpriteBatch.test.ts → glQuadBatchWriter.test.ts} +74 -53
  104. package/src/glRasterShapeRenderer.test.ts +109 -0
  105. package/src/glScale9Shape.test.ts +0 -13
  106. package/src/glShape.test.ts +166 -21
  107. package/src/glShapeData.test.ts +74 -0
  108. package/src/glShapeMesh.test.ts +1 -1
  109. package/src/glShapeRasterizer.test.ts +32 -0
  110. package/src/glSprite.test.ts +71 -62
  111. package/src/glStandardMaterial.test.ts +3 -3
  112. package/src/glTextLabel.test.ts +16 -16
  113. package/src/glTilemap.test.ts +44 -47
  114. package/src/glVelocity.test.ts +4 -4
  115. package/dist/glBitmap.d.ts +0 -4
  116. package/dist/glBitmap.d.ts.map +0 -1
  117. package/dist/glBitmap.js +0 -93
  118. package/dist/glBitmap.js.map +0 -1
  119. package/dist/glRenderTargetNode2D.d.ts +0 -11
  120. package/dist/glRenderTargetNode2D.d.ts.map +0 -1
  121. package/dist/glRenderTargetNode2D.js +0 -78
  122. package/dist/glRenderTargetNode2D.js.map +0 -1
  123. package/dist/glSpriteBatch.d.ts.map +0 -1
  124. package/dist/glSpriteBatch.js.map +0 -1
  125. package/dist/glSpriteRenderer.d.ts +0 -3
  126. package/dist/glSpriteRenderer.d.ts.map +0 -1
  127. package/dist/glSpriteRenderer.js +0 -54
  128. package/dist/glSpriteRenderer.js.map +0 -1
  129. package/dist/glVideo.d.ts +0 -6
  130. package/dist/glVideo.d.ts.map +0 -1
  131. package/dist/glVideo.js +0 -63
  132. package/dist/glVideo.js.map +0 -1
  133. package/src/glBitmap.test.ts +0 -136
  134. package/src/glRenderTargetNode2D.test.ts +0 -328
  135. package/src/glSpriteRenderer.test.ts +0 -89
  136. 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, ImageResource, Material } from '@flighthq/types/contract';
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
- flushGlSpriteBatch,
10
- packGlSpriteBatchMaterialInstance,
11
- prepareGlSpriteBatchWrite,
12
- recordGlSpriteBatchColorScaleBias,
8
+ flushGlQuadBatchWriter,
9
+ packGlQuadBatchMaterialInstance,
10
+ prepareGlQuadBatchWrite as prepareResolvedGlQuadBatchWrite,
11
+ recordGlQuadBatchColorScaleBias,
13
12
  setGlQuadBatchWorldAndTexture,
14
13
  useGlQuadBatchProgram,
15
- } from './glSpriteBatch';
14
+ } from './glQuadBatchWriter';
16
15
  import { standardGlMaterialRenderer } from './glStandardMaterial';
17
16
  import { createGlState } from './glTestHelper';
18
17
 
19
- function makeTexture(): ImageResource {
20
- return createImageResource(document.createElement('img'));
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('flushGlSpriteBatch', () => {
101
+ describe('flushGlQuadBatchWriter', () => {
81
102
  it('does nothing when batch count is zero', () => {
82
103
  const { state, gl } = createGlState();
83
- flushGlSpriteBatch(state);
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
- prepareGlSpriteBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1);
93
- runtime.spriteBatchCount = 1;
94
- flushGlSpriteBatch(state);
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.spriteBatchCount).toBe(0);
98
- expect(runtime.spriteBatchTexture).toBeNull();
99
- expect(runtime.spriteBatchBlendMode).toBeNull();
100
- expect(runtime.spriteBatchMaterial).toBeNull();
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('packGlSpriteBatchMaterialInstance', () => {
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).spriteBatchMaterialRenderer = null;
108
- expect(() => packGlSpriteBatchMaterialInstance(state, null, 0)).not.toThrow();
128
+ getGlRenderStateRuntime(state).quadBatchWriterMaterialRenderer = null;
129
+ expect(() => packGlQuadBatchMaterialInstance(state, null, 0)).not.toThrow();
109
130
  });
110
131
  });
111
132
 
112
- describe('prepareGlSpriteBatchWrite', () => {
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
- prepareGlSpriteBatchWrite(state, makeTexture(), null, null, standardGlMaterialRenderer, 1);
139
+ prepareGlQuadBatchWrite(state, makeTexture(), null, null, standardGlMaterialRenderer, 1);
119
140
 
120
- expect(runtime.flushPendingDraws).toBe(flushGlSpriteBatch);
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 = prepareGlSpriteBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 2);
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
- prepareGlSpriteBatchWrite(state, tex1, null, null, standardGlMaterialRenderer, 1);
138
- runtime.spriteBatchCount = 1;
158
+ prepareGlQuadBatchWrite(state, tex1, null, null, standardGlMaterialRenderer, 1);
159
+ runtime.quadBatchWriterCount = 1;
139
160
 
140
- prepareGlSpriteBatchWrite(state, tex2, null, null, standardGlMaterialRenderer, 1);
161
+ prepareGlQuadBatchWrite(state, tex2, null, null, standardGlMaterialRenderer, 1);
141
162
 
142
163
  expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
143
- expect(runtime.spriteBatchTexture).toBe(tex2);
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
- prepareGlSpriteBatchWrite(state, tex, null, materialA, standardGlMaterialRenderer, 1);
154
- runtime.spriteBatchCount = 1;
174
+ prepareGlQuadBatchWrite(state, tex, null, materialA, standardGlMaterialRenderer, 1);
175
+ runtime.quadBatchWriterCount = 1;
155
176
 
156
- prepareGlSpriteBatchWrite(state, tex, null, materialB, standardGlMaterialRenderer, 1);
177
+ prepareGlQuadBatchWrite(state, tex, null, materialB, standardGlMaterialRenderer, 1);
157
178
 
158
179
  expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
159
- expect(runtime.spriteBatchMaterial).toBe(materialB);
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.spriteBatchInstanceData.length;
187
+ const initialFloats = runtime.quadBatchWriterInstanceData.length;
167
188
 
168
- prepareGlSpriteBatchWrite(state, tex, null, null, standardGlMaterialRenderer, initialFloats + 100);
189
+ prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, initialFloats + 100);
169
190
 
170
- expect(runtime.spriteBatchInstanceData.length).toBeGreaterThan(initialFloats);
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
- prepareGlSpriteBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, false);
180
- runtime.spriteBatchCount = 1;
200
+ prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, false);
201
+ runtime.quadBatchWriterCount = 1;
181
202
 
182
- prepareGlSpriteBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, true);
203
+ prepareGlQuadBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, true);
183
204
 
184
205
  expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
185
- expect(runtime.spriteBatchSmoothing).toBe(true);
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
- prepareGlSpriteBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, true);
194
- runtime.spriteBatchCount = 1;
195
- prepareGlSpriteBatchWrite(state, tex, null, null, standardGlMaterialRenderer, 1, true);
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('recordGlSpriteBatchColorScaleBias', () => {
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
- prepareGlSpriteBatchWrite(state, makeTexture(), null, null, standardGlMaterialRenderer, 1);
206
- recordGlSpriteBatchColorScaleBias(state, ct(0.5), 0);
207
- runtime.spriteBatchCount = 1;
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.spriteBatchColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
210
- flushGlSpriteBatch(state);
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(() => recordGlSpriteBatchColorScaleBias(state, null, 0)).not.toThrow();
219
- expect(runtime.spriteBatchColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
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
- recordGlSpriteBatchColorScaleBias(state, ct(0.5), 0);
227
- expect(runtime.spriteBatchColorScaleBiasMode).toBe(CT_MODE_UNIFORM);
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
- });
@@ -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 { flushGlSpriteBatch } from './glSpriteBatch';
9
- import { registerStandardGlMaterial } from './glStandardMaterial';
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
- const canvas = document.createElement('canvas');
34
- canvas.width = 1;
35
- canvas.height = 1;
36
- const ctx = canvas.getContext('2d')!;
37
- return { canvas, ctx, image: createImageResource(canvas), lastContentId: -1, lastW: 0, lastH: 0 };
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
- registerStandardGlMaterial(state);
214
+ registerGlShapeRasterizer(state, noopRasterizer);
215
+ registerGlStandardMaterial(state);
76
216
  drawGlShape(state, makeShapeNode({ commands: [] }, makeShapeData()));
77
- expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
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
- registerStandardGlMaterial(state);
222
+ registerGlShapeRasterizer(state, noopRasterizer);
223
+ registerGlStandardMaterial(state);
83
224
  drawGlShape(state, makeShapeNode({ commands: [{}] }, null));
84
- expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
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).spriteBatchCount).toBe(0);
232
+ expect(getGlRenderStateRuntime(state).quadBatchWriterCount).toBe(0);
91
233
  });
92
234
 
93
- it('writes one instance to the sprite batch when shape has valid commands and bounds', () => {
235
+ it('writes one instance to the quad-batch writer when shape has valid commands and bounds', () => {
94
236
  const { state } = createGlState();
95
- registerStandardGlMaterial(state);
237
+ registerGlShapeRasterizer(state, noopRasterizer);
238
+ registerGlStandardMaterial(state);
96
239
  drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
97
- expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
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
- registerStandardGlMaterial(state);
245
+ registerGlShapeRasterizer(state, noopRasterizer);
246
+ registerGlStandardMaterial(state);
103
247
  drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
104
- flushGlSpriteBatch(state);
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
- registerStandardGlMaterial(state);
254
+ registerGlShapeRasterizer(state, noopRasterizer);
255
+ registerGlStandardMaterial(state);
111
256
  drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
112
- const d = getGlRenderStateRuntime(state).spriteBatchInstanceData;
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
  });