@flighthq/displayobject-gl 0.1.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 (143) hide show
  1. package/dist/glBitmap.d.ts +4 -0
  2. package/dist/glBitmap.d.ts.map +1 -0
  3. package/dist/glBitmap.js +106 -0
  4. package/dist/glBitmap.js.map +1 -0
  5. package/dist/glCache.d.ts +30 -0
  6. package/dist/glCache.d.ts.map +1 -0
  7. package/dist/glCache.js +185 -0
  8. package/dist/glCache.js.map +1 -0
  9. package/dist/glClip.d.ts +3 -0
  10. package/dist/glClip.d.ts.map +1 -0
  11. package/dist/glClip.js +46 -0
  12. package/dist/glClip.js.map +1 -0
  13. package/dist/glClipContours.d.ts +4 -0
  14. package/dist/glClipContours.d.ts.map +1 -0
  15. package/dist/glClipContours.js +148 -0
  16. package/dist/glClipContours.js.map +1 -0
  17. package/dist/glClipRectangle.d.ts +4 -0
  18. package/dist/glClipRectangle.d.ts.map +1 -0
  19. package/dist/glClipRectangle.js +68 -0
  20. package/dist/glClipRectangle.js.map +1 -0
  21. package/dist/glColorTransformMaterial.d.ts +4 -0
  22. package/dist/glColorTransformMaterial.d.ts.map +1 -0
  23. package/dist/glColorTransformMaterial.js +116 -0
  24. package/dist/glColorTransformMaterial.js.map +1 -0
  25. package/dist/glDefaultMaterial.d.ts +4 -0
  26. package/dist/glDefaultMaterial.d.ts.map +1 -0
  27. package/dist/glDefaultMaterial.js +20 -0
  28. package/dist/glDefaultMaterial.js.map +1 -0
  29. package/dist/glDisplayObject.d.ts +5 -0
  30. package/dist/glDisplayObject.d.ts.map +1 -0
  31. package/dist/glDisplayObject.js +41 -0
  32. package/dist/glDisplayObject.js.map +1 -0
  33. package/dist/glDisplayObjectRegistration.d.ts +25 -0
  34. package/dist/glDisplayObjectRegistration.d.ts.map +1 -0
  35. package/dist/glDisplayObjectRegistration.js +51 -0
  36. package/dist/glDisplayObjectRegistration.js.map +1 -0
  37. package/dist/glMaterials.d.ts +4 -0
  38. package/dist/glMaterials.d.ts.map +1 -0
  39. package/dist/glMaterials.js +97 -0
  40. package/dist/glMaterials.js.map +1 -0
  41. package/dist/glParticleEmitter.d.ts +4 -0
  42. package/dist/glParticleEmitter.d.ts.map +1 -0
  43. package/dist/glParticleEmitter.js +258 -0
  44. package/dist/glParticleEmitter.js.map +1 -0
  45. package/dist/glQuadBatch.d.ts +3 -0
  46. package/dist/glQuadBatch.d.ts.map +1 -0
  47. package/dist/glQuadBatch.js +88 -0
  48. package/dist/glQuadBatch.js.map +1 -0
  49. package/dist/glRichText.d.ts +9 -0
  50. package/dist/glRichText.d.ts.map +1 -0
  51. package/dist/glRichText.js +149 -0
  52. package/dist/glRichText.js.map +1 -0
  53. package/dist/glScale9Mapper.d.ts +3 -0
  54. package/dist/glScale9Mapper.d.ts.map +1 -0
  55. package/dist/glScale9Mapper.js +37 -0
  56. package/dist/glScale9Mapper.js.map +1 -0
  57. package/dist/glScale9Shape.d.ts +9 -0
  58. package/dist/glScale9Shape.d.ts.map +1 -0
  59. package/dist/glScale9Shape.js +118 -0
  60. package/dist/glScale9Shape.js.map +1 -0
  61. package/dist/glShape.d.ts +4 -0
  62. package/dist/glShape.d.ts.map +1 -0
  63. package/dist/glShape.js +125 -0
  64. package/dist/glShape.js.map +1 -0
  65. package/dist/glShapeMesh.d.ts +9 -0
  66. package/dist/glShapeMesh.d.ts.map +1 -0
  67. package/dist/glShapeMesh.js +90 -0
  68. package/dist/glShapeMesh.js.map +1 -0
  69. package/dist/glSprite.d.ts +3 -0
  70. package/dist/glSprite.d.ts.map +1 -0
  71. package/dist/glSprite.js +28 -0
  72. package/dist/glSprite.js.map +1 -0
  73. package/dist/glSpriteBatch.d.ts +11 -0
  74. package/dist/glSpriteBatch.d.ts.map +1 -0
  75. package/dist/glSpriteBatch.js +241 -0
  76. package/dist/glSpriteBatch.js.map +1 -0
  77. package/dist/glSpriteRenderer.d.ts +3 -0
  78. package/dist/glSpriteRenderer.d.ts.map +1 -0
  79. package/dist/glSpriteRenderer.js +50 -0
  80. package/dist/glSpriteRenderer.js.map +1 -0
  81. package/dist/glTestHelper.d.ts +9 -0
  82. package/dist/glTestHelper.d.ts.map +1 -0
  83. package/dist/glTestHelper.js +17 -0
  84. package/dist/glTestHelper.js.map +1 -0
  85. package/dist/glTextInput.d.ts +4 -0
  86. package/dist/glTextInput.d.ts.map +1 -0
  87. package/dist/glTextInput.js +43 -0
  88. package/dist/glTextInput.js.map +1 -0
  89. package/dist/glTextLabel.d.ts +4 -0
  90. package/dist/glTextLabel.d.ts.map +1 -0
  91. package/dist/glTextLabel.js +130 -0
  92. package/dist/glTextLabel.js.map +1 -0
  93. package/dist/glTilemap.d.ts +3 -0
  94. package/dist/glTilemap.d.ts.map +1 -0
  95. package/dist/glTilemap.js +74 -0
  96. package/dist/glTilemap.js.map +1 -0
  97. package/dist/glUniformColorTransformMaterial.d.ts +4 -0
  98. package/dist/glUniformColorTransformMaterial.d.ts.map +1 -0
  99. package/dist/glUniformColorTransformMaterial.js +56 -0
  100. package/dist/glUniformColorTransformMaterial.js.map +1 -0
  101. package/dist/glVelocity.d.ts +22 -0
  102. package/dist/glVelocity.d.ts.map +1 -0
  103. package/dist/glVelocity.js +319 -0
  104. package/dist/glVelocity.js.map +1 -0
  105. package/dist/glVideo.d.ts +6 -0
  106. package/dist/glVideo.d.ts.map +1 -0
  107. package/dist/glVideo.js +60 -0
  108. package/dist/glVideo.js.map +1 -0
  109. package/dist/index.d.ts +28 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +29 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/moduleMockTestHelper.d.ts +15 -0
  114. package/dist/moduleMockTestHelper.d.ts.map +1 -0
  115. package/dist/moduleMockTestHelper.js +25 -0
  116. package/dist/moduleMockTestHelper.js.map +1 -0
  117. package/package.json +51 -0
  118. package/src/glBitmap.test.ts +128 -0
  119. package/src/glCache.test.ts +230 -0
  120. package/src/glClip.test.ts +101 -0
  121. package/src/glClipContours.test.ts +61 -0
  122. package/src/glClipRectangle.test.ts +47 -0
  123. package/src/glColorTransformMaterial.test.ts +45 -0
  124. package/src/glDefaultMaterial.test.ts +19 -0
  125. package/src/glDisplayObject.test.ts +85 -0
  126. package/src/glDisplayObjectRegistration.test.ts +130 -0
  127. package/src/glMaterials.test.ts +91 -0
  128. package/src/glParticleEmitter.test.ts +98 -0
  129. package/src/glQuadBatch.test.ts +132 -0
  130. package/src/glRichText.test.ts +135 -0
  131. package/src/glScale9Mapper.test.ts +20 -0
  132. package/src/glScale9Shape.test.ts +90 -0
  133. package/src/glShape.test.ts +115 -0
  134. package/src/glShapeMesh.test.ts +64 -0
  135. package/src/glSprite.test.ts +79 -0
  136. package/src/glSpriteBatch.test.ts +153 -0
  137. package/src/glSpriteRenderer.test.ts +75 -0
  138. package/src/glTextInput.test.ts +51 -0
  139. package/src/glTextLabel.test.ts +151 -0
  140. package/src/glTilemap.test.ts +127 -0
  141. package/src/glUniformColorTransformMaterial.test.ts +24 -0
  142. package/src/glVelocity.test.ts +155 -0
  143. package/src/glVideo.test.ts +65 -0
@@ -0,0 +1,135 @@
1
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
2
+ import { createRichText } from '@flighthq/text';
3
+ import { enableTextInput } from '@flighthq/textinput';
4
+ import type { RendererData, RenderProxy2D, RichText } from '@flighthq/types';
5
+
6
+ import {
7
+ createGlRichTextData,
8
+ defaultGlRichTextRenderer,
9
+ destroyGlRichTextData,
10
+ drawGlRichText,
11
+ drawGlRichTextWithOverlay,
12
+ registerGlTextInputOverlay,
13
+ } from './glRichText';
14
+ import { createGlState } from './glTestHelper';
15
+
16
+ function makeRichTextNode(): RenderProxy2D {
17
+ const richText = createRichText();
18
+ return {
19
+ source: richText,
20
+ blendMode: 0,
21
+ alpha: 1,
22
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
23
+ rendererData: { texture: null },
24
+ } as unknown as RenderProxy2D;
25
+ }
26
+
27
+ describe('createGlRichTextData', () => {
28
+ it('allocates per-node data with no texture yet', () => {
29
+ const { state } = createGlState();
30
+ const data = createGlRichTextData(state, createRichText()) as unknown as { texture: WebGLTexture | null };
31
+ expect(data.texture).toBeNull();
32
+ });
33
+ });
34
+
35
+ describe('defaultGlRichTextRenderer', () => {
36
+ it('has a createData function', () => {
37
+ expect(typeof defaultGlRichTextRenderer.createData).toBe('function');
38
+ });
39
+
40
+ it('has a submit function pointing to drawGlRichText', () => {
41
+ expect(defaultGlRichTextRenderer.submit).toBe(drawGlRichText);
42
+ });
43
+ });
44
+
45
+ describe('destroyGlRichTextData', () => {
46
+ it('deletes the GPU texture the node owns', () => {
47
+ const { state, gl } = createGlState();
48
+ const texture = gl.createTexture();
49
+ const deleteSpy = vi.spyOn(gl, 'deleteTexture');
50
+ destroyGlRichTextData(state, { texture } as unknown as RendererData);
51
+ expect(deleteSpy).toHaveBeenCalledWith(texture);
52
+ });
53
+
54
+ it('is a no-op when no texture was allocated', () => {
55
+ const { state, gl } = createGlState();
56
+ const deleteSpy = vi.spyOn(gl, 'deleteTexture');
57
+ destroyGlRichTextData(state, { texture: null } as unknown as RendererData);
58
+ expect(deleteSpy).not.toHaveBeenCalled();
59
+ });
60
+ });
61
+
62
+ describe('drawGlRichText', () => {
63
+ it('binds the active bitmap shader when drawing rich text', () => {
64
+ const { state } = createGlState();
65
+ const renderProxy = makeRichTextNode();
66
+ (renderProxy.source as RichText).data.text = 'hello';
67
+
68
+ const bindSpy = vi.spyOn(getGlRenderStateRuntime(state).defaultBitmapShader, 'bind');
69
+ drawGlRichText(state, renderProxy);
70
+
71
+ expect(bindSpy).toHaveBeenCalledWith(state.gl, state, renderProxy);
72
+ });
73
+
74
+ it('returns early without drawing when text and chrome are empty', () => {
75
+ const { state, gl } = createGlState();
76
+ drawGlRichText(state, makeRichTextNode());
77
+ expect(gl.drawElements).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('draws when text is non-empty', () => {
81
+ const { state, gl } = createGlState();
82
+ const renderProxy = makeRichTextNode();
83
+ (renderProxy.source as RichText).data.text = 'hello';
84
+ drawGlRichText(state, renderProxy);
85
+ expect(gl.drawElements).toHaveBeenCalled();
86
+ });
87
+
88
+ it('draws resolved htmlText spans', () => {
89
+ const { state, gl } = createGlState();
90
+ const renderProxy = makeRichTextNode();
91
+ (renderProxy.source as RichText).data.htmlText = '<b>Bold</b><font color="#00ff00">Green</font>';
92
+ drawGlRichText(state, renderProxy);
93
+ expect(gl.drawElements).toHaveBeenCalled();
94
+ });
95
+
96
+ it('draws field chrome even when text is empty', () => {
97
+ const { state, gl } = createGlState();
98
+ const renderProxy = makeRichTextNode();
99
+ (renderProxy.source as RichText).data.background = true;
100
+ drawGlRichText(state, renderProxy);
101
+ expect(gl.drawElements).toHaveBeenCalled();
102
+ });
103
+ });
104
+
105
+ describe('drawGlRichTextWithOverlay', () => {
106
+ it('runs an optional canvas overlay after layout', () => {
107
+ const { state } = createGlState();
108
+ const renderProxy = makeRichTextNode();
109
+ (renderProxy.source as RichText).data.text = 'hello';
110
+ const overlay = vi.fn();
111
+
112
+ drawGlRichTextWithOverlay(state, renderProxy, overlay);
113
+
114
+ expect(overlay).toHaveBeenCalled();
115
+ });
116
+ });
117
+
118
+ describe('registerGlTextInputOverlay', () => {
119
+ it('invokes the registered overlay only for a RichText with an input slot', () => {
120
+ const overlay = vi.fn();
121
+ registerGlTextInputOverlay(overlay);
122
+ const { state } = createGlState();
123
+
124
+ const plain = makeRichTextNode();
125
+ (plain.source as RichText).data.text = 'x';
126
+ drawGlRichText(state, plain);
127
+ expect(overlay).not.toHaveBeenCalled();
128
+
129
+ const editable = makeRichTextNode();
130
+ (editable.source as RichText).data.text = 'x';
131
+ enableTextInput(editable.source as RichText);
132
+ drawGlRichText(state, editable);
133
+ expect(overlay).toHaveBeenCalled();
134
+ });
135
+ });
@@ -0,0 +1,20 @@
1
+ import { createRectangle } from '@flighthq/geometry';
2
+
3
+ import { buildGlScale9Mapper } from './glScale9Mapper';
4
+
5
+ describe('buildGlScale9Mapper', () => {
6
+ it('returns null for invalid scale or bounds', () => {
7
+ expect(buildGlScale9Mapper(createRectangle(0, 0, 0, 100), createRectangle(10, 10, 20, 20), 1, 1)).toBeNull();
8
+ expect(buildGlScale9Mapper(createRectangle(0, 0, 100, 100), createRectangle(10, 10, 20, 20), 0, 1)).toBeNull();
9
+ });
10
+
11
+ it('maps edge and center coordinates using 9-slice scaling', () => {
12
+ const mapper = buildGlScale9Mapper(createRectangle(0, 0, 100, 100), createRectangle(10, 10, 20, 20), 2, 3)!;
13
+
14
+ expect(mapper.mapX(5)).toBe(5);
15
+ expect(mapper.mapX(20)).toBe(70);
16
+ expect(mapper.mapX(40)).toBe(140);
17
+ expect(mapper.mapY(20)).toBe(120);
18
+ expect(mapper.mapY(40)).toBe(240);
19
+ });
20
+ });
@@ -0,0 +1,90 @@
1
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
2
+ import { appendShapeBeginFill, appendShapeRectangle, createScale9Shape } from '@flighthq/shape';
3
+
4
+ import {
5
+ createGlScale9ShapeData,
6
+ defaultGlScale9ShapeRenderer,
7
+ destroyGlScale9ShapeData,
8
+ drawGlScale9Shape,
9
+ drawGlScale9ShapeMask,
10
+ remapGlScale9Commands,
11
+ } from './glScale9Shape';
12
+ import { createGlState } from './glTestHelper';
13
+
14
+ const grid = { height: 80, width: 80, x: 10, y: 10 };
15
+
16
+ describe('createGlScale9ShapeData', () => {
17
+ it('creates renderer data with a Gl texture', () => {
18
+ const { state, gl } = createGlState();
19
+ const shape = createScale9Shape(grid);
20
+
21
+ const data = createGlScale9ShapeData(state, shape);
22
+
23
+ expect(data).not.toBeNull();
24
+ expect(gl.createTexture).toHaveBeenCalled();
25
+ });
26
+ });
27
+
28
+ describe('defaultGlScale9ShapeRenderer', () => {
29
+ it('has submit and createData functions', () => {
30
+ expect(defaultGlScale9ShapeRenderer.createData).toBe(createGlScale9ShapeData);
31
+ expect(defaultGlScale9ShapeRenderer.submit).toBe(drawGlScale9Shape);
32
+ });
33
+ });
34
+
35
+ describe('destroyGlScale9ShapeData', () => {
36
+ it('deletes the texture it owns', () => {
37
+ const { state, gl } = createGlState();
38
+ const data = createGlScale9ShapeData(state, createScale9Shape(grid))!;
39
+ destroyGlScale9ShapeData(state, data);
40
+ expect(gl.deleteTexture).toHaveBeenCalled();
41
+ });
42
+ });
43
+
44
+ describe('drawGlScale9Shape', () => {
45
+ it('returns early when commands are empty', () => {
46
+ const { state, gl } = createGlState();
47
+ const shape = createScale9Shape(grid);
48
+ const data = getOrCreateRenderProxy2D(state, shape);
49
+
50
+ drawGlScale9Shape(state, data);
51
+
52
+ expect(gl.drawElements).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it('returns early when rendererData is null', () => {
56
+ const { state, gl } = createGlState();
57
+ const shape = createScale9Shape(grid);
58
+ appendShapeBeginFill(shape, 0xff0000);
59
+ appendShapeRectangle(shape, 0, 0, 100, 100);
60
+ const data = getOrCreateRenderProxy2D(state, shape);
61
+
62
+ drawGlScale9Shape(state, data);
63
+
64
+ expect(gl.drawElements).not.toHaveBeenCalled();
65
+ });
66
+ });
67
+
68
+ describe('drawGlScale9ShapeMask', () => {
69
+ it('uses the same draw path as normal Scale9 rendering', () => {
70
+ const { state, gl } = createGlState();
71
+ const shape = createScale9Shape(grid);
72
+ const data = getOrCreateRenderProxy2D(state, shape);
73
+
74
+ drawGlScale9ShapeMask(state, data);
75
+
76
+ expect(gl.drawElements).not.toHaveBeenCalled();
77
+ });
78
+ });
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
+ });
@@ -0,0 +1,115 @@
1
+ import type * as FlightNodeModule from '@flighthq/node';
2
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
3
+ import type { RenderProxy2D } from '@flighthq/types';
4
+ import { BatchFormat } from '@flighthq/types';
5
+
6
+ import { registerDefaultGlMaterial } from './glDefaultMaterial';
7
+ import type * as GlShapeModule from './glShape';
8
+ import { flushGlSpriteBatch } from './glSpriteBatch';
9
+ import { createGlState } from './glTestHelper';
10
+ import { scopeModuleMocks } from './moduleMockTestHelper';
11
+
12
+ // @flighthq/node's bounds/revision queries expect a real BoundsNode; these tests drive drawGlShape
13
+ // with lightweight fake proxies, so the two queries are stubbed. scopeModuleMocks scopes the stub to
14
+ // this file (registry reset before the mock applies, unmock + reset after), so under a shared
15
+ // (isolate:false) worker it never leaks into the many real consumers of these functions (node,
16
+ // interaction, shape, text) — and a sibling that pre-evaluated ./glShape still picks up the stub.
17
+ let defaultGlShapeRenderer: typeof GlShapeModule.defaultGlShapeRenderer;
18
+ let drawGlShape: typeof GlShapeModule.drawGlShape;
19
+
20
+ scopeModuleMocks(['@flighthq/node']);
21
+
22
+ beforeAll(async () => {
23
+ vi.doMock('@flighthq/node', async (importOriginal) => ({
24
+ ...(await importOriginal<typeof FlightNodeModule>()),
25
+ getNodeLocalBoundsRectangle: () => ({ x: 0, y: 0, width: 64, height: 48 }),
26
+ getNodeLocalContentRevision: (source: any) => source?.data?.version ?? 0,
27
+ }));
28
+ ({ defaultGlShapeRenderer, drawGlShape } = await import('./glShape'));
29
+ });
30
+
31
+ function makeShapeData() {
32
+ const canvas = document.createElement('canvas');
33
+ canvas.width = 1;
34
+ canvas.height = 1;
35
+ const ctx = canvas.getContext('2d')!;
36
+ return { canvas, ctx, lastContentId: -1, lastW: 0, lastH: 0 };
37
+ }
38
+
39
+ function makeShapeNode(data: Record<string, unknown> = {}, rendererData: unknown = null): RenderProxy2D {
40
+ return {
41
+ source: {
42
+ data: {
43
+ commands: [],
44
+ version: 0,
45
+ ...data,
46
+ },
47
+ },
48
+ rendererData: rendererData,
49
+ blendMode: 0,
50
+ alpha: 1,
51
+ material: null,
52
+ materialData: null,
53
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
54
+ } as unknown as RenderProxy2D;
55
+ }
56
+
57
+ describe('defaultGlShapeRenderer', () => {
58
+ it('declares BatchFormat.Quad', () => {
59
+ expect(defaultGlShapeRenderer.format).toBe(BatchFormat.Quad);
60
+ });
61
+
62
+ it('has a createData function', () => {
63
+ expect(typeof defaultGlShapeRenderer.createData).toBe('function');
64
+ });
65
+
66
+ it('has a submit function pointing to drawGlShape', () => {
67
+ expect(defaultGlShapeRenderer.submit).toBe(drawGlShape);
68
+ });
69
+ });
70
+
71
+ describe('drawGlShape', () => {
72
+ it('returns early without writing to batch when commands array is empty', () => {
73
+ const { state } = createGlState();
74
+ registerDefaultGlMaterial(state);
75
+ drawGlShape(state, makeShapeNode({ commands: [] }, makeShapeData()));
76
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
77
+ });
78
+
79
+ it('returns early without writing to batch when rendererData is null', () => {
80
+ const { state } = createGlState();
81
+ registerDefaultGlMaterial(state);
82
+ drawGlShape(state, makeShapeNode({ commands: [{}] }, null));
83
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
84
+ });
85
+
86
+ it('returns early without writing to batch when no material renderer is registered', () => {
87
+ const { state } = createGlState();
88
+ drawGlShape(state, makeShapeNode({ commands: [{}] }, makeShapeData()));
89
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
90
+ });
91
+
92
+ it('writes one instance to the sprite batch when shape has valid commands and bounds', () => {
93
+ const { state } = createGlState();
94
+ registerDefaultGlMaterial(state);
95
+ drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
96
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
97
+ });
98
+
99
+ it('draws via drawElementsInstanced after flush', () => {
100
+ const { state, gl } = createGlState();
101
+ registerDefaultGlMaterial(state);
102
+ drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
103
+ flushGlSpriteBatch(state);
104
+ expect(gl.drawElementsInstanced).toHaveBeenCalled();
105
+ });
106
+
107
+ it('writes correct size into instance data', () => {
108
+ const { state } = createGlState();
109
+ registerDefaultGlMaterial(state);
110
+ drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
111
+ const d = getGlRenderStateRuntime(state).spriteBatchInstanceData;
112
+ expect(d[6]).toBe(64); // width from mocked bounds
113
+ expect(d[7]).toBe(48); // height from mocked bounds
114
+ });
115
+ });
@@ -0,0 +1,64 @@
1
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
2
+ import type { RenderProxy2D } from '@flighthq/types';
3
+
4
+ import { drawGlShapeMeshes } from './glShapeMesh';
5
+ import { createGlState } from './glTestHelper';
6
+
7
+ function makeProxy(): RenderProxy2D {
8
+ return {
9
+ alpha: 1,
10
+ blendMode: null,
11
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
12
+ } as unknown as RenderProxy2D;
13
+ }
14
+
15
+ const TRIANGLE = {
16
+ vertices: new Float32Array([0, 0, 10, 0, 0, 10]),
17
+ indices: new Uint16Array([0, 1, 2]),
18
+ color: 0xff8040,
19
+ alpha: 1,
20
+ };
21
+
22
+ describe('drawGlShapeMeshes', () => {
23
+ it('binds the mesh program and draws each mesh', () => {
24
+ const { state, gl } = createGlState();
25
+
26
+ drawGlShapeMeshes(state, makeProxy(), [TRIANGLE]);
27
+
28
+ expect(gl.useProgram).toHaveBeenCalled();
29
+ expect(gl.drawElements).toHaveBeenCalledWith(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
30
+ });
31
+
32
+ it('records the mesh program as currentProgram so content draws re-bind', () => {
33
+ const { state } = createGlState();
34
+ expect(getGlRenderStateRuntime(state).currentProgram).toBeNull();
35
+
36
+ drawGlShapeMeshes(state, makeProxy(), [TRIANGLE]);
37
+
38
+ expect(getGlRenderStateRuntime(state).currentProgram).not.toBeNull();
39
+ });
40
+
41
+ it('uploads premultiplied color (color * alpha) for the standard blend', () => {
42
+ const { state, gl } = createGlState();
43
+
44
+ drawGlShapeMeshes(state, makeProxy(), [{ ...TRIANGLE, color: 0xffffff, alpha: 0.5 }]);
45
+
46
+ expect(gl.uniform4f).toHaveBeenCalledWith(expect.anything(), 0.5, 0.5, 0.5, 0.5);
47
+ });
48
+
49
+ it('skips fully transparent meshes', () => {
50
+ const { state, gl } = createGlState();
51
+
52
+ drawGlShapeMeshes(state, makeProxy(), [{ ...TRIANGLE, alpha: 0 }]);
53
+
54
+ expect(gl.drawElements).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it('is a no-op for an empty mesh list', () => {
58
+ const { state, gl } = createGlState();
59
+
60
+ drawGlShapeMeshes(state, makeProxy(), []);
61
+
62
+ expect(gl.useProgram).not.toHaveBeenCalled();
63
+ });
64
+ });
@@ -0,0 +1,79 @@
1
+ import { addNodeChild } from '@flighthq/node';
2
+ import { getOrCreateRenderProxy2D, prepareDisplayObjectRender, 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
+ prepareDisplayObjectRender(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
+ prepareDisplayObjectRender(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
+ prepareDisplayObjectRender(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
+ prepareDisplayObjectRender(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
+ prepareDisplayObjectRender(state, parent);
75
+ renderGlSprite(state, parent);
76
+
77
+ expect(renderer.submit).toHaveBeenCalledTimes(2);
78
+ });
79
+ });
@@ -0,0 +1,153 @@
1
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
2
+ import type { Material } from '@flighthq/types';
3
+
4
+ import { defaultGlMaterialRenderer } from './glDefaultMaterial';
5
+ import {
6
+ bindGlQuadBatchBaseAttributes,
7
+ ensureGlQuadBatchShader,
8
+ flushGlSpriteBatch,
9
+ packGlSpriteBatchMaterialInstance,
10
+ prepareGlSpriteBatchWrite,
11
+ setGlQuadBatchWorldAndTexture,
12
+ useGlQuadBatchProgram,
13
+ } from './glSpriteBatch';
14
+ import { createGlState } from './glTestHelper';
15
+
16
+ function makeTexture(): HTMLImageElement {
17
+ return document.createElement('img');
18
+ }
19
+
20
+ function makeMaterial(): Material {
21
+ return { kind: 'TestMaterial' } as Material;
22
+ }
23
+
24
+ describe('bindGlQuadBatchBaseAttributes', () => {
25
+ it('sets up the corner and base instance attribute pointers', () => {
26
+ const { state, gl } = createGlState();
27
+ ensureGlQuadBatchShader(state);
28
+ bindGlQuadBatchBaseAttributes(state, 0);
29
+ expect(gl.vertexAttribPointer).toHaveBeenCalled();
30
+ expect(gl.vertexAttribDivisor).toHaveBeenCalled();
31
+ });
32
+ });
33
+
34
+ describe('ensureGlQuadBatchShader', () => {
35
+ it('returns a shader with a program and attribute locations', () => {
36
+ const { state } = createGlState();
37
+ const shader = ensureGlQuadBatchShader(state);
38
+ expect(shader.program).toBeDefined();
39
+ expect(typeof shader.locCorner).toBe('number');
40
+ expect(shader.locWorldMatrix).toBeDefined();
41
+ expect(shader.locTexture).toBeDefined();
42
+ });
43
+
44
+ it('is idempotent — returns the same shader on repeated calls', () => {
45
+ const { state } = createGlState();
46
+ const s1 = ensureGlQuadBatchShader(state);
47
+ const s2 = ensureGlQuadBatchShader(state);
48
+ expect(s1).toBe(s2);
49
+ });
50
+ });
51
+
52
+ describe('flushGlSpriteBatch', () => {
53
+ it('does nothing when batch count is zero', () => {
54
+ const { state, gl } = createGlState();
55
+ flushGlSpriteBatch(state);
56
+ expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it('draws when instances are pending and resets state', () => {
60
+ const { state, gl } = createGlState();
61
+ const runtime = getGlRenderStateRuntime(state);
62
+ const tex = makeTexture();
63
+
64
+ prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 1);
65
+ runtime.spriteBatchCount = 1;
66
+ flushGlSpriteBatch(state);
67
+
68
+ expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 1);
69
+ expect(runtime.spriteBatchCount).toBe(0);
70
+ expect(runtime.spriteBatchTexture).toBeNull();
71
+ expect(runtime.spriteBatchBlendMode).toBeNull();
72
+ expect(runtime.spriteBatchMaterial).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe('packGlSpriteBatchMaterialInstance', () => {
77
+ it('is a no-op when no per-instance material renderer is active', () => {
78
+ const { state } = createGlState();
79
+ getGlRenderStateRuntime(state).spriteBatchMaterialRenderer = null;
80
+ expect(() => packGlSpriteBatchMaterialInstance(state, null, 0)).not.toThrow();
81
+ });
82
+ });
83
+
84
+ describe('prepareGlSpriteBatchWrite', () => {
85
+ it('returns float index 0 for an empty batch', () => {
86
+ const { state } = createGlState();
87
+ const tex = makeTexture();
88
+
89
+ const base = prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, 2);
90
+ expect(base).toBe(0);
91
+ });
92
+
93
+ it('flushes when texture changes', () => {
94
+ const { state, gl } = createGlState();
95
+ const runtime = getGlRenderStateRuntime(state);
96
+ const tex1 = makeTexture();
97
+ const tex2 = makeTexture();
98
+
99
+ prepareGlSpriteBatchWrite(state, tex1, null, null, defaultGlMaterialRenderer, 1);
100
+ runtime.spriteBatchCount = 1;
101
+
102
+ prepareGlSpriteBatchWrite(state, tex2, null, null, defaultGlMaterialRenderer, 1);
103
+
104
+ expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
105
+ expect(runtime.spriteBatchTexture).toBe(tex2);
106
+ });
107
+
108
+ it('flushes when material changes', () => {
109
+ const { state, gl } = createGlState();
110
+ const runtime = getGlRenderStateRuntime(state);
111
+ const tex = makeTexture();
112
+ const materialA = makeMaterial();
113
+ const materialB = makeMaterial();
114
+
115
+ prepareGlSpriteBatchWrite(state, tex, null, materialA, defaultGlMaterialRenderer, 1);
116
+ runtime.spriteBatchCount = 1;
117
+
118
+ prepareGlSpriteBatchWrite(state, tex, null, materialB, defaultGlMaterialRenderer, 1);
119
+
120
+ expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
121
+ expect(runtime.spriteBatchMaterial).toBe(materialB);
122
+ });
123
+
124
+ it('grows instance data when capacity is exceeded', () => {
125
+ const { state } = createGlState();
126
+ const runtime = getGlRenderStateRuntime(state);
127
+ const tex = makeTexture();
128
+ const initialFloats = runtime.spriteBatchInstanceData.length;
129
+
130
+ prepareGlSpriteBatchWrite(state, tex, null, null, defaultGlMaterialRenderer, initialFloats + 100);
131
+
132
+ expect(runtime.spriteBatchInstanceData.length).toBeGreaterThan(initialFloats);
133
+ });
134
+ });
135
+
136
+ describe('setGlQuadBatchWorldAndTexture', () => {
137
+ it('uploads the world matrix and texture unit', () => {
138
+ const { state, gl } = createGlState();
139
+ setGlQuadBatchWorldAndTexture(state, {} as WebGLUniformLocation, {} as WebGLUniformLocation);
140
+ expect(gl.uniformMatrix3fv).toHaveBeenCalled();
141
+ expect(gl.uniform1i).toHaveBeenCalled();
142
+ });
143
+ });
144
+
145
+ describe('useGlQuadBatchProgram', () => {
146
+ it('binds the program and records it as current', () => {
147
+ const { state, gl } = createGlState();
148
+ const program = {} as WebGLProgram;
149
+ useGlQuadBatchProgram(state, program);
150
+ expect(gl.useProgram).toHaveBeenCalledWith(program);
151
+ expect(getGlRenderStateRuntime(state).currentProgram).toBe(program);
152
+ });
153
+ });