@flighthq/scene2d-gl 0.2.1-next.840.857425c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/enableGlColorAdjustmentGuards.d.ts +4 -0
  2. package/dist/enableGlColorAdjustmentGuards.d.ts.map +1 -0
  3. package/dist/enableGlColorAdjustmentGuards.js +21 -0
  4. package/dist/enableGlColorAdjustmentGuards.js.map +1 -0
  5. package/dist/glBitmap.d.ts +4 -0
  6. package/dist/glBitmap.d.ts.map +1 -0
  7. package/dist/glBitmap.js +93 -0
  8. package/dist/glBitmap.js.map +1 -0
  9. package/dist/glCache.d.ts +30 -0
  10. package/dist/glCache.d.ts.map +1 -0
  11. package/dist/glCache.js +196 -0
  12. package/dist/glCache.js.map +1 -0
  13. package/dist/glClip.d.ts +3 -0
  14. package/dist/glClip.d.ts.map +1 -0
  15. package/dist/glClip.js +46 -0
  16. package/dist/glClip.js.map +1 -0
  17. package/dist/glClipContours.d.ts +4 -0
  18. package/dist/glClipContours.d.ts.map +1 -0
  19. package/dist/glClipContours.js +148 -0
  20. package/dist/glClipContours.js.map +1 -0
  21. package/dist/glClipRectangle.d.ts +4 -0
  22. package/dist/glClipRectangle.d.ts.map +1 -0
  23. package/dist/glClipRectangle.js +68 -0
  24. package/dist/glClipRectangle.js.map +1 -0
  25. package/dist/glColorAdjustment.d.ts +3 -0
  26. package/dist/glColorAdjustment.d.ts.map +1 -0
  27. package/dist/glColorAdjustment.js +361 -0
  28. package/dist/glColorAdjustment.js.map +1 -0
  29. package/dist/glDefaultMaterial.d.ts +4 -0
  30. package/dist/glDefaultMaterial.d.ts.map +1 -0
  31. package/dist/glDefaultMaterial.js +20 -0
  32. package/dist/glDefaultMaterial.js.map +1 -0
  33. package/dist/glNode2D.d.ts +5 -0
  34. package/dist/glNode2D.d.ts.map +1 -0
  35. package/dist/glNode2D.js +41 -0
  36. package/dist/glNode2D.js.map +1 -0
  37. package/dist/glNode2DRegistration.d.ts +25 -0
  38. package/dist/glNode2DRegistration.d.ts.map +1 -0
  39. package/dist/glNode2DRegistration.js +51 -0
  40. package/dist/glNode2DRegistration.js.map +1 -0
  41. package/dist/glParticleEmitter2D.d.ts +4 -0
  42. package/dist/glParticleEmitter2D.d.ts.map +1 -0
  43. package/dist/glParticleEmitter2D.js +263 -0
  44. package/dist/glParticleEmitter2D.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 +92 -0
  48. package/dist/glQuadBatch.js.map +1 -0
  49. package/dist/glRichText.d.ts +8 -0
  50. package/dist/glRichText.d.ts.map +1 -0
  51. package/dist/glRichText.js +161 -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 +146 -0
  64. package/dist/glShape.js.map +1 -0
  65. package/dist/glShapeMesh.d.ts +5 -0
  66. package/dist/glShapeMesh.d.ts.map +1 -0
  67. package/dist/glShapeMesh.js +114 -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 +277 -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 +54 -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 +141 -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 +79 -0
  96. package/dist/glTilemap.js.map +1 -0
  97. package/dist/glVelocity.d.ts +22 -0
  98. package/dist/glVelocity.d.ts.map +1 -0
  99. package/dist/glVelocity.js +319 -0
  100. package/dist/glVelocity.js.map +1 -0
  101. package/dist/glVideo.d.ts +6 -0
  102. package/dist/glVideo.d.ts.map +1 -0
  103. package/dist/glVideo.js +62 -0
  104. package/dist/glVideo.js.map +1 -0
  105. package/dist/index.d.ts +27 -0
  106. package/dist/index.d.ts.map +1 -0
  107. package/dist/index.js +28 -0
  108. package/dist/index.js.map +1 -0
  109. package/dist/moduleMockTestHelper.d.ts +15 -0
  110. package/dist/moduleMockTestHelper.d.ts.map +1 -0
  111. package/dist/moduleMockTestHelper.js +25 -0
  112. package/dist/moduleMockTestHelper.js.map +1 -0
  113. package/package.json +60 -0
  114. package/src/enableGlColorAdjustmentGuards.test.ts +76 -0
  115. package/src/glBitmap.test.ts +136 -0
  116. package/src/glCache.test.ts +234 -0
  117. package/src/glClip.test.ts +101 -0
  118. package/src/glClipContours.test.ts +61 -0
  119. package/src/glClipRectangle.test.ts +47 -0
  120. package/src/glColorAdjustment.test.ts +166 -0
  121. package/src/glDefaultMaterial.test.ts +19 -0
  122. package/src/glNode2D.test.ts +85 -0
  123. package/src/glNode2DRegistration.test.ts +132 -0
  124. package/src/glParticleEmitter2D.test.ts +122 -0
  125. package/src/glQuadBatch.test.ts +132 -0
  126. package/src/glRichText.test.ts +140 -0
  127. package/src/glScale9Mapper.test.ts +20 -0
  128. package/src/glScale9Shape.test.ts +90 -0
  129. package/src/glShape.test.ts +116 -0
  130. package/src/glShapeMesh.test.ts +164 -0
  131. package/src/glSprite.test.ts +79 -0
  132. package/src/glSpriteBatch.test.ts +249 -0
  133. package/src/glSpriteRenderer.test.ts +89 -0
  134. package/src/glTextInput.test.ts +51 -0
  135. package/src/glTextLabel.test.ts +156 -0
  136. package/src/glTilemap.test.ts +127 -0
  137. package/src/glVelocity.test.ts +156 -0
  138. package/src/glVideo.test.ts +98 -0
@@ -0,0 +1,25 @@
1
+ import { afterAll, beforeAll, vi } from 'vitest';
2
+ /**
3
+ * Makes vi.doMock-based module mocking safe under the shared (isolate:false) worker the root vitest
4
+ * config uses. Call once at the top of a test file with every module id the file mocks, then apply
5
+ * the mocks and dynamically import the subject inside a beforeAll — both must stay in the test file
6
+ * so relative ids resolve against it. (This helper is colocated in the same package src, so the
7
+ * doUnmock ids passed here resolve to the same modules the test's doMock ids do.)
8
+ *
9
+ * It brackets the file with two vi.resetModules() calls. The first is registered here so it runs
10
+ * before the file's own beforeAll applies the mocks: it forces a subject a sibling already evaluated
11
+ * against the real modules to re-evaluate under the mocks (otherwise the dynamic import returns the
12
+ * cached, real-bound copy). The second, after unmocking, drops the mocked instances so a later file
13
+ * re-imports the real modules instead of the mock left in the shared registry.
14
+ */
15
+ export function scopeModuleMocks(mockedIds) {
16
+ beforeAll(() => {
17
+ vi.resetModules();
18
+ });
19
+ afterAll(() => {
20
+ for (const id of mockedIds)
21
+ vi.doUnmock(id);
22
+ vi.resetModules();
23
+ });
24
+ }
25
+ //# sourceMappingURL=moduleMockTestHelper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moduleMockTestHelper.js","sourceRoot":"","sources":["../src/moduleMockTestHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAA4B;IAC3D,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,YAAY,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;IACH,QAAQ,CAAC,GAAG,EAAE;QACZ,KAAK,MAAM,EAAE,IAAI,SAAS;YAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC5C,EAAE,CAAC,YAAY,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@flighthq/scene2d-gl",
3
+ "version": "0.2.1-next.840.857425c",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/scene2d-gl"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src/**/*.test.ts",
21
+ "!dist/**/*.test.js",
22
+ "!dist/**/*.test.d.ts",
23
+ "!dist/**/*.test.js.map",
24
+ "!dist/**/*.test.d.ts.map"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -b",
28
+ "clean": "tsc -b --clean",
29
+ "test": "vitest run --config vitest.config.ts",
30
+ "test:watch": "vitest --watch --config vitest.config.ts",
31
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
32
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
+ },
34
+ "dependencies": {
35
+ "@flighthq/color": "0.2.1-next.840.857425c",
36
+ "@flighthq/scene2d": "0.2.1-next.840.857425c",
37
+ "@flighthq/scene2d-canvas": "0.2.1-next.840.857425c",
38
+ "@flighthq/geometry": "0.2.1-next.840.857425c",
39
+ "@flighthq/image": "0.2.1-next.840.857425c",
40
+ "@flighthq/log": "0.2.1-next.840.857425c",
41
+ "@flighthq/node": "0.2.1-next.840.857425c",
42
+ "@flighthq/path": "0.2.1-next.840.857425c",
43
+ "@flighthq/render": "0.2.1-next.840.857425c",
44
+ "@flighthq/render-gl": "0.2.1-next.840.857425c",
45
+ "@flighthq/shape": "0.2.1-next.840.857425c",
46
+ "@flighthq/text": "0.2.1-next.840.857425c",
47
+ "@flighthq/textinput": "0.2.1-next.840.857425c",
48
+ "@flighthq/textlayout": "0.2.1-next.840.857425c",
49
+ "@flighthq/texture": "0.2.1-next.840.857425c",
50
+ "@flighthq/types": "0.2.1-next.840.857425c",
51
+ "@flighthq/velocity": "0.2.1-next.840.857425c"
52
+ },
53
+ "devDependencies": {
54
+ "@flighthq/particleemitter": "*",
55
+ "@flighthq/sprite": "*",
56
+ "typescript": "^5.3.0"
57
+ },
58
+ "description": "WebGL2 display-object renderers (bitmap, shape, sprite, text, tilemap, particles)",
59
+ "sideEffects": false
60
+ }
@@ -0,0 +1,76 @@
1
+ import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log';
2
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
3
+ import type { ColorTransform } from '@flighthq/types';
4
+
5
+ import { areGlColorAdjustmentGuardsEnabled, enableGlColorAdjustmentGuards } from './enableGlColorAdjustmentGuards';
6
+ import { recordGlSpriteBatchColorTransform } from './glSpriteBatch';
7
+ import { createGlState } from './glTestHelper';
8
+
9
+ function ct(): ColorTransform {
10
+ return {
11
+ redMultiplier: 0.5,
12
+ greenMultiplier: 0.5,
13
+ blueMultiplier: 0.5,
14
+ alphaMultiplier: 1,
15
+ redOffset: 0,
16
+ greenOffset: 0,
17
+ blueOffset: 0,
18
+ alphaOffset: 0,
19
+ } as ColorTransform;
20
+ }
21
+
22
+ describe('areGlColorAdjustmentGuardsEnabled', () => {
23
+ it('reports false until guards are installed, then true', () => {
24
+ const { state } = createGlState();
25
+ expect(areGlColorAdjustmentGuardsEnabled(state)).toBe(false);
26
+ enableGlColorAdjustmentGuards(state);
27
+ expect(areGlColorAdjustmentGuardsEnabled(state)).toBe(true);
28
+ });
29
+ });
30
+
31
+ describe('enableGlColorAdjustmentGuards', () => {
32
+ it('warns once when a color transform is recorded but color adjustment was never enabled', () => {
33
+ const { state } = createGlState();
34
+ const sink = createMemoryLogSink(8);
35
+ addLogSink(sink.sink);
36
+ try {
37
+ enableGlColorAdjustmentGuards(state);
38
+ recordGlSpriteBatchColorTransform(state, ct(), 0);
39
+ const entries = getMemoryLogSinkEntries(sink);
40
+ expect(entries.length).toBe(1);
41
+ const data = entries[0].data as Record<string, unknown>;
42
+ expect(String(data.message)).toContain('enableGlColorAdjustment');
43
+ } finally {
44
+ removeLogSink(sink.sink);
45
+ }
46
+ });
47
+
48
+ it('stays silent for an untinted instance', () => {
49
+ const { state } = createGlState();
50
+ const sink = createMemoryLogSink(8);
51
+ addLogSink(sink.sink);
52
+ try {
53
+ enableGlColorAdjustmentGuards(state);
54
+ recordGlSpriteBatchColorTransform(state, null, 0);
55
+ expect(getMemoryLogSinkEntries(sink).length).toBe(0);
56
+ } finally {
57
+ removeLogSink(sink.sink);
58
+ }
59
+ });
60
+
61
+ it('does not warn when the guard slot is present but color adjustment is also enabled', () => {
62
+ const { state } = createGlState();
63
+ const runtime = getGlRenderStateRuntime(state);
64
+ const sink = createMemoryLogSink(8);
65
+ addLogSink(sink.sink);
66
+ try {
67
+ enableGlColorAdjustmentGuards(state);
68
+ // Simulate the fold being installed so the dispatcher never reaches the guard branch.
69
+ runtime.glColorAdjustmentFold = { drawShapeMeshes: () => {}, flush: () => false, record: () => {} };
70
+ recordGlSpriteBatchColorTransform(state, ct(), 0);
71
+ expect(getMemoryLogSinkEntries(sink).length).toBe(0);
72
+ } finally {
73
+ removeLogSink(sink.sink);
74
+ }
75
+ });
76
+ });
@@ -0,0 +1,136 @@
1
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
2
+ import type { RenderProxy2D } from '@flighthq/types';
3
+ import { BatchFormat } from '@flighthq/types';
4
+
5
+ import { defaultGlBitmapRenderer, drawGlBitmap } from './glBitmap';
6
+ import { registerDefaultGlMaterial } from './glDefaultMaterial';
7
+ import { flushGlSpriteBatch } from './glSpriteBatch';
8
+ import { createGlState } from './glTestHelper';
9
+
10
+ function makeBitmapData() {
11
+ return { lastSrc: null, lastVersion: -1 };
12
+ }
13
+
14
+ function makeRenderProxy(image: unknown = null, rendererData: unknown = makeBitmapData()): RenderProxy2D {
15
+ return {
16
+ source: { data: { image } },
17
+ blendMode: 0,
18
+ alpha: 1,
19
+ material: null,
20
+ materialData: null,
21
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 20 },
22
+ rendererData,
23
+ } as unknown as RenderProxy2D;
24
+ }
25
+
26
+ function makeImageResource(source: unknown = null, width = 32, height = 32, data: Uint8ClampedArray | null = null) {
27
+ return { source, data, compressed: null, width, height, version: 1, alphaType: 'straight' };
28
+ }
29
+
30
+ describe('defaultGlBitmapRenderer', () => {
31
+ it('declares BatchFormat.Quad', () => {
32
+ expect(defaultGlBitmapRenderer.format).toBe(BatchFormat.Quad);
33
+ });
34
+
35
+ it('has a createData function', () => {
36
+ expect(typeof defaultGlBitmapRenderer.createData).toBe('function');
37
+ });
38
+
39
+ it('has a destroyData function', () => {
40
+ expect(typeof defaultGlBitmapRenderer.destroyData).toBe('function');
41
+ });
42
+
43
+ it('has a submit function pointing to drawGlBitmap', () => {
44
+ expect(defaultGlBitmapRenderer.submit).toBe(drawGlBitmap);
45
+ });
46
+ });
47
+
48
+ describe('drawGlBitmap', () => {
49
+ it('returns early without writing to batch when image is null', () => {
50
+ const { state } = createGlState();
51
+ registerDefaultGlMaterial(state);
52
+ drawGlBitmap(state, makeRenderProxy(null));
53
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
54
+ });
55
+
56
+ it('returns early without writing to batch when the image has neither source nor data', () => {
57
+ const { state } = createGlState();
58
+ registerDefaultGlMaterial(state);
59
+ drawGlBitmap(state, makeRenderProxy(makeImageResource(null)));
60
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
61
+ });
62
+
63
+ it('writes one instance for a data-only image (a generated Surface with no element)', () => {
64
+ const { state } = createGlState();
65
+ registerDefaultGlMaterial(state);
66
+ const data = new Uint8ClampedArray(32 * 32 * 4);
67
+ drawGlBitmap(state, makeRenderProxy(makeImageResource(null, 32, 32, data)));
68
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
69
+ });
70
+
71
+ it('writes one instance to the sprite batch when image is valid', () => {
72
+ const { state } = createGlState();
73
+ registerDefaultGlMaterial(state);
74
+ const img = document.createElement('img');
75
+ drawGlBitmap(state, makeRenderProxy(makeImageResource(img, 32, 32)));
76
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
77
+ });
78
+
79
+ it('draws via drawElementsInstanced after flush', () => {
80
+ const { state, gl } = createGlState();
81
+ registerDefaultGlMaterial(state);
82
+ const img = document.createElement('img');
83
+ drawGlBitmap(state, makeRenderProxy(makeImageResource(img)));
84
+ flushGlSpriteBatch(state as any);
85
+ expect(gl.drawElementsInstanced).toHaveBeenCalled();
86
+ });
87
+
88
+ it('writes correct transform and size into instance data', () => {
89
+ const { state } = createGlState();
90
+ registerDefaultGlMaterial(state);
91
+ const img = document.createElement('img');
92
+ drawGlBitmap(state, makeRenderProxy(makeImageResource(img, 64, 48)));
93
+ const d = getGlRenderStateRuntime(state).spriteBatchInstanceData;
94
+ expect(d[0]).toBe(1); // a
95
+ expect(d[1]).toBe(0); // b
96
+ expect(d[2]).toBe(0); // c
97
+ expect(d[3]).toBe(1); // d
98
+ expect(d[4]).toBe(10); // tx
99
+ expect(d[5]).toBe(20); // ty
100
+ expect(d[6]).toBe(64); // width
101
+ expect(d[7]).toBe(48); // height
102
+ expect(d[8]).toBeCloseTo(0); // u0
103
+ expect(d[9]).toBeCloseTo(0); // v0
104
+ expect(d[10]).toBeCloseTo(1); // u1
105
+ expect(d[11]).toBeCloseTo(1); // v1
106
+ expect(d[12]).toBe(1); // alpha
107
+ });
108
+
109
+ it('writes source-rectangle UVs when sourceRectangle is set', () => {
110
+ const { state } = createGlState();
111
+ registerDefaultGlMaterial(state);
112
+ const img = document.createElement('img');
113
+ const proxy = {
114
+ ...makeRenderProxy({
115
+ source: img,
116
+ width: 128,
117
+ height: 64,
118
+ sourceRectangle: { x: 16, y: 8, width: 32, height: 16 },
119
+ }),
120
+ source: {
121
+ data: {
122
+ image: { source: img, width: 128, height: 64 },
123
+ sourceRectangle: { x: 16, y: 8, width: 32, height: 16 },
124
+ },
125
+ },
126
+ } as unknown as RenderProxy2D;
127
+ drawGlBitmap(state, proxy);
128
+ const d = getGlRenderStateRuntime(state).spriteBatchInstanceData;
129
+ expect(d[6]).toBe(32); // width = sr.width
130
+ expect(d[7]).toBe(16); // height = sr.height
131
+ expect(d[8]).toBeCloseTo(16 / 128); // u0
132
+ expect(d[9]).toBeCloseTo(8 / 64); // v0
133
+ expect(d[10]).toBeCloseTo(48 / 128); // u1
134
+ expect(d[11]).toBeCloseTo(24 / 64); // v1
135
+ });
136
+ });
@@ -0,0 +1,234 @@
1
+ import { createMatrix } from '@flighthq/geometry';
2
+ import { createRenderCache, createRenderState, RenderCacheKind, useRenderCache } from '@flighthq/render';
3
+ import type * as GlRenderGlModule from '@flighthq/render-gl';
4
+ import { createDisplayObject } from '@flighthq/scene2d';
5
+ import type { GlRenderState, GlRenderTarget } from '@flighthq/types';
6
+ import { EntityRuntimeKey } from '@flighthq/types';
7
+
8
+ import type * as GlCacheModule from './glCache';
9
+ import type * as GlNode2DModule from './glNode2D';
10
+ import type * as GlSpriteBatchModule from './glSpriteBatch';
11
+ import { scopeModuleMocks } from './moduleMockTestHelper';
12
+
13
+ // The GL render-target lifecycle (@flighthq/render-gl) and the two local collaborators
14
+ // ./glSpriteBatch and ./glNode2D are stubbed so cache orchestration can be unit-tested
15
+ // without a real GL pipeline: createGlRenderTarget returns a plain descriptor, and the composite,
16
+ // batch-flush, and subtree-render calls become spies for the call and ordering assertions below.
17
+ // The mocks are scoped to this file's dynamic import of ./glCache and unmocked in afterAll, so under
18
+ // a shared (isolate:false) worker they never leak into the real render-gl / scene2d-gl
19
+ // consumers. The mocked functions are read back from the same dynamic imports so the handles the
20
+ // assertions observe are the exact vi.fn instances the cache module calls.
21
+ let createGlCacheState: typeof GlCacheModule.createGlCacheState;
22
+ let defaultGlRenderCacheRenderer: typeof GlCacheModule.defaultGlRenderCacheRenderer;
23
+ let enableGlRenderCache: typeof GlCacheModule.enableGlRenderCache;
24
+ let ensureGlRenderCacheTarget: typeof GlCacheModule.ensureGlRenderCacheTarget;
25
+ let getGlRenderCacheTarget: typeof GlCacheModule.getGlRenderCacheTarget;
26
+ let refreshGlRenderCache: typeof GlCacheModule.refreshGlRenderCache;
27
+ let releaseGlRenderCache: typeof GlCacheModule.releaseGlRenderCache;
28
+ let createGlRenderStateRuntime: typeof GlRenderGlModule.createGlRenderStateRuntime;
29
+ let getGlRenderStateRuntime: typeof GlRenderGlModule.getGlRenderStateRuntime;
30
+ let destroyGlRenderTarget: typeof GlRenderGlModule.destroyGlRenderTarget;
31
+ let drawGlRenderTargetResult: typeof GlRenderGlModule.drawGlRenderTargetResult;
32
+ let renderGlScene2D: typeof GlNode2DModule.renderGlScene2D;
33
+ let flushGlSpriteBatch: typeof GlSpriteBatchModule.flushGlSpriteBatch;
34
+
35
+ // EntityRuntimeKey (Symbol.for) and RenderCacheKind (a string) are identity-stable across the
36
+ // registry reset scopeModuleMocks performs, and cache adapters are stored on the state, not module-
37
+ // level, so the statically-imported @flighthq/render still interoperates with the re-evaluated
38
+ // subject even though the subject re-imports @flighthq/render under the reset.
39
+ scopeModuleMocks(['./glSpriteBatch', '@flighthq/render-gl', './glNode2D']);
40
+
41
+ beforeAll(async () => {
42
+ vi.doMock('./glSpriteBatch', async (importOriginal) => {
43
+ const actual = await importOriginal<typeof GlSpriteBatchModule>();
44
+ return { ...actual, flushGlSpriteBatch: vi.fn() };
45
+ });
46
+ vi.doMock('@flighthq/render-gl', async (importOriginal) => {
47
+ const actual = await importOriginal<typeof GlRenderGlModule>();
48
+ return {
49
+ ...actual,
50
+ beginGlRenderPass: vi.fn(),
51
+ setGlRenderTransform2D: vi.fn(),
52
+ createGlRenderTarget: vi.fn((_state: unknown, descriptor: { width: number; height: number }): GlRenderTarget => {
53
+ const texture = {} as WebGLTexture;
54
+ return {
55
+ framebuffer: {} as WebGLFramebuffer,
56
+ resolveFramebuffer: null,
57
+ texture,
58
+ textures: [texture],
59
+ depthTexture: null,
60
+ colorRenderbuffers: [],
61
+ depthStencilRenderbuffer: null,
62
+ format: 'rgba8',
63
+ colorSpace: 'srgb',
64
+ clearColors: [],
65
+ clearDepth: 1,
66
+ sampleCount: 1,
67
+ width: descriptor.width,
68
+ height: descriptor.height,
69
+ };
70
+ }),
71
+ destroyGlRenderTarget: vi.fn(),
72
+ drawGlRenderTargetResult: vi.fn(),
73
+ endGlRenderPass: vi.fn(),
74
+ resizeGlRenderTarget: vi.fn((_state: unknown, target: GlRenderTarget, width: number, height: number) => {
75
+ target.width = width;
76
+ target.height = height;
77
+ }),
78
+ };
79
+ });
80
+ vi.doMock('./glNode2D', async (importOriginal) => {
81
+ const actual = await importOriginal<typeof GlNode2DModule>();
82
+ return { ...actual, renderGlScene2D: vi.fn() };
83
+ });
84
+
85
+ ({ createGlRenderStateRuntime, getGlRenderStateRuntime, destroyGlRenderTarget, drawGlRenderTargetResult } =
86
+ await import('@flighthq/render-gl'));
87
+ ({ flushGlSpriteBatch } = await import('./glSpriteBatch'));
88
+ ({ renderGlScene2D } = await import('./glNode2D'));
89
+ ({
90
+ createGlCacheState,
91
+ defaultGlRenderCacheRenderer,
92
+ enableGlRenderCache,
93
+ ensureGlRenderCacheTarget,
94
+ getGlRenderCacheTarget,
95
+ refreshGlRenderCache,
96
+ releaseGlRenderCache,
97
+ } = await import('./glCache'));
98
+ });
99
+
100
+ function fakeScreen(options = {}): GlRenderState {
101
+ const state = createRenderState(options) as unknown as GlRenderState;
102
+ (state as any).gl = { clear: vi.fn(), clearColor: vi.fn(), COLOR_BUFFER_BIT: 0x4000 };
103
+ state[EntityRuntimeKey] = createGlRenderStateRuntime();
104
+ return state;
105
+ }
106
+
107
+ function makeCacheNode(source: unknown): any {
108
+ return { source, kind: RenderCacheKind, transform2D: createMatrix(), alpha: 1, blendMode: null };
109
+ }
110
+
111
+ describe('createGlCacheState', () => {
112
+ it('copies renderers and shares the GL context but keeps its own node map', () => {
113
+ const screen = fakeScreen();
114
+ enableGlRenderCache(screen);
115
+ const cacheState = createGlCacheState(screen);
116
+ expect(getGlRenderStateRuntime(cacheState).rendererMap.get(RenderCacheKind)).toBe(defaultGlRenderCacheRenderer);
117
+ expect((cacheState as any).gl).toBe((screen as any).gl);
118
+ expect(getGlRenderStateRuntime(cacheState).renderProxyMap).not.toBe(getGlRenderStateRuntime(screen).renderProxyMap);
119
+ });
120
+ });
121
+
122
+ describe('defaultGlRenderCacheRenderer', () => {
123
+ it('does nothing when no cache is attached to the source', () => {
124
+ const state = fakeScreen();
125
+ defaultGlRenderCacheRenderer.submit(state, makeCacheNode(createDisplayObject()));
126
+ expect(drawGlRenderTargetResult).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it('composites the cache target attached to the source node', () => {
130
+ const state = fakeScreen();
131
+ const obj = createDisplayObject();
132
+ const cache = createRenderCache();
133
+ useRenderCache(state, obj, cache);
134
+ const target = ensureGlRenderCacheTarget(state, cache, 16, 16);
135
+ defaultGlRenderCacheRenderer.submit(state, makeCacheNode(obj));
136
+ expect(drawGlRenderTargetResult).toHaveBeenCalledWith(state, expect.anything(), target, expect.anything());
137
+ });
138
+
139
+ it('flushes pending batched geometry before the immediate composite', () => {
140
+ const state = fakeScreen();
141
+ const obj = createDisplayObject();
142
+ const cache = createRenderCache();
143
+ useRenderCache(state, obj, cache);
144
+ ensureGlRenderCacheTarget(state, cache, 16, 16);
145
+ defaultGlRenderCacheRenderer.submit(state, makeCacheNode(obj));
146
+ // The composite draws an immediate quad outside the sprite batch; geometry submitted earlier in
147
+ // the walk must be drained first, or it replays after the cache result (a doubled image).
148
+ expect(flushGlSpriteBatch).toHaveBeenCalledWith(state);
149
+ expect((flushGlSpriteBatch as any).mock.invocationCallOrder[0]).toBeLessThan(
150
+ (drawGlRenderTargetResult as any).mock.invocationCallOrder[0],
151
+ );
152
+ });
153
+ });
154
+
155
+ describe('enableGlRenderCache', () => {
156
+ it('registers the renderer for the render cache kind', () => {
157
+ const state = fakeScreen();
158
+ enableGlRenderCache(state);
159
+ expect(getGlRenderStateRuntime(state).rendererMap.get(RenderCacheKind)).toBe(defaultGlRenderCacheRenderer);
160
+ });
161
+ });
162
+
163
+ describe('ensureGlRenderCacheTarget', () => {
164
+ it('creates a target sized to the request', () => {
165
+ const state = fakeScreen();
166
+ const target = ensureGlRenderCacheTarget(state, createRenderCache(), 64, 32);
167
+ expect(target.width).toBe(64);
168
+ expect(target.height).toBe(32);
169
+ });
170
+
171
+ it('reuses and resizes the same target on subsequent calls', () => {
172
+ const state = fakeScreen();
173
+ const cache = createRenderCache();
174
+ const first = ensureGlRenderCacheTarget(state, cache, 64, 32);
175
+ const second = ensureGlRenderCacheTarget(state, cache, 16, 16);
176
+ expect(second).toBe(first);
177
+ expect(second.width).toBe(16);
178
+ });
179
+
180
+ it('keeps targets isolated per state for the same handle', () => {
181
+ const stateA = fakeScreen();
182
+ const stateB = fakeScreen();
183
+ const cache = createRenderCache();
184
+ expect(ensureGlRenderCacheTarget(stateA, cache, 8, 8)).not.toBe(ensureGlRenderCacheTarget(stateB, cache, 8, 8));
185
+ });
186
+ });
187
+
188
+ describe('getGlRenderCacheTarget', () => {
189
+ it('returns null before a target is allocated', () => {
190
+ expect(getGlRenderCacheTarget(fakeScreen(), createRenderCache())).toBeNull();
191
+ });
192
+
193
+ it('returns the allocated target', () => {
194
+ const state = fakeScreen();
195
+ const cache = createRenderCache();
196
+ const target = ensureGlRenderCacheTarget(state, cache, 8, 8);
197
+ expect(getGlRenderCacheTarget(state, cache)).toBe(target);
198
+ });
199
+ });
200
+
201
+ describe('refreshGlRenderCache', () => {
202
+ it('bakes on the first call and allocates the target on the screen state', () => {
203
+ const screen = fakeScreen();
204
+ const cacheState = createGlCacheState(screen);
205
+ const cache = createRenderCache();
206
+ const obj = createDisplayObject();
207
+ const rebaked = refreshGlRenderCache(cacheState, cache, obj, { padding: 5 });
208
+ expect(rebaked).toBe(true);
209
+ expect(renderGlScene2D).toHaveBeenCalled();
210
+ const target = getGlRenderCacheTarget(screen, cache);
211
+ expect(target).not.toBeNull();
212
+ expect(target!.width).toBe(10);
213
+ });
214
+
215
+ it('skips the bake under requiresInvalidation when nothing changed', () => {
216
+ const screen = fakeScreen({ sceneGraphSyncPolicy: 'requiresInvalidation' });
217
+ const cacheState = createGlCacheState(screen);
218
+ const cache = createRenderCache();
219
+ const obj = createDisplayObject();
220
+ refreshGlRenderCache(cacheState, cache, obj, { padding: 5 });
221
+ expect(refreshGlRenderCache(cacheState, cache, obj, { padding: 5 })).toBe(false);
222
+ });
223
+ });
224
+
225
+ describe('releaseGlRenderCache', () => {
226
+ it('destroys and drops the target for the cache', () => {
227
+ const state = fakeScreen();
228
+ const cache = createRenderCache();
229
+ const target = ensureGlRenderCacheTarget(state, cache, 8, 8);
230
+ releaseGlRenderCache(state, cache);
231
+ expect(destroyGlRenderTarget).toHaveBeenCalledWith(state, target);
232
+ expect(getGlRenderCacheTarget(state, cache)).toBeNull();
233
+ });
234
+ });
@@ -0,0 +1,101 @@
1
+ import { createMatrix, createRectangle } from '@flighthq/geometry';
2
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
3
+ import { createDisplayObject } from '@flighthq/scene2d';
4
+ import type { ClipRegion, Node2D, RenderProxy2D } from '@flighthq/types';
5
+
6
+ import { enableGlClipSupport } from './glClip';
7
+ import { createGlState } from './glTestHelper';
8
+
9
+ function makeRectClip(): ClipRegion {
10
+ return { rect: createRectangle(0, 0, 50, 50), contours: null, winding: 'nonZero', version: 0 };
11
+ }
12
+
13
+ function makeContourClip(): ClipRegion {
14
+ return {
15
+ rect: createRectangle(0, 0, 50, 50),
16
+ contours: [[0, 0, 50, 0, 50, 50, 0, 50]],
17
+ winding: 'nonZero',
18
+ version: 0,
19
+ };
20
+ }
21
+
22
+ function makeProxy(source: Node2D, clipDepth: number): RenderProxy2D {
23
+ return { source, transform2D: createMatrix(), clipDepth } as unknown as RenderProxy2D;
24
+ }
25
+
26
+ describe('enableGlClipSupport', () => {
27
+ it('installs the clip hooks on the render state', () => {
28
+ const { state } = createGlState();
29
+
30
+ enableGlClipSupport(state);
31
+
32
+ expect(state.displayObjectClipHooks).not.toBeNull();
33
+ expect(typeof state.displayObjectClipHooks?.pushClip).toBe('function');
34
+ expect(typeof state.displayObjectClipHooks?.popClip).toBe('function');
35
+ expect(typeof state.displayObjectClipHooks?.finalize).toBe('function');
36
+ });
37
+
38
+ it('pushClip enables the scissor test for a rectangle clip', () => {
39
+ const { state, gl } = createGlState();
40
+ enableGlClipSupport(state);
41
+ const source = createDisplayObject();
42
+ source.clip = makeRectClip();
43
+
44
+ state.displayObjectClipHooks?.pushClip(state, makeProxy(source, 1), source);
45
+
46
+ expect(gl.enable).toHaveBeenLastCalledWith(gl.SCISSOR_TEST);
47
+ expect(getGlRenderStateRuntime(state).clipForms).toEqual(['rect']);
48
+ });
49
+
50
+ it('pushClip enables the stencil test for a contour clip', () => {
51
+ const { state, gl } = createGlState();
52
+ enableGlClipSupport(state);
53
+ const source = createDisplayObject();
54
+ source.clip = makeContourClip();
55
+
56
+ state.displayObjectClipHooks?.pushClip(state, makeProxy(source, 1), source);
57
+
58
+ expect(gl.enable).toHaveBeenCalledWith(gl.STENCIL_TEST);
59
+ const runtime = getGlRenderStateRuntime(state);
60
+ expect(runtime.clipForms).toEqual(['contour']);
61
+ expect(runtime.currentMaskDepth).toBe(1);
62
+ });
63
+
64
+ it('pushClip does nothing when the source has no clip', () => {
65
+ const { state } = createGlState();
66
+ enableGlClipSupport(state);
67
+ const source = createDisplayObject();
68
+
69
+ state.displayObjectClipHooks?.pushClip(state, makeProxy(source, 0), source);
70
+
71
+ expect(getGlRenderStateRuntime(state).clipForms).toEqual([]);
72
+ });
73
+
74
+ it('popClip unwinds forms down to the source parent depth', () => {
75
+ const { state } = createGlState();
76
+ enableGlClipSupport(state);
77
+ const source = createDisplayObject();
78
+ source.clip = makeRectClip();
79
+ state.displayObjectClipHooks?.pushClip(state, makeProxy(source, 1), source);
80
+
81
+ // A sibling at the same depth carrying its own clip pops the previous one first.
82
+ state.displayObjectClipHooks?.popClip(state, makeProxy(source, 1), source);
83
+
84
+ expect(getGlRenderStateRuntime(state).clipForms).toEqual([]);
85
+ });
86
+
87
+ it('finalize pops every remaining clip form', () => {
88
+ const { state } = createGlState();
89
+ enableGlClipSupport(state);
90
+ const rectSource = createDisplayObject();
91
+ rectSource.clip = makeRectClip();
92
+ const contourSource = createDisplayObject();
93
+ contourSource.clip = makeContourClip();
94
+ state.displayObjectClipHooks?.pushClip(state, makeProxy(rectSource, 1), rectSource);
95
+ state.displayObjectClipHooks?.pushClip(state, makeProxy(contourSource, 2), contourSource);
96
+
97
+ state.displayObjectClipHooks?.finalize(state);
98
+
99
+ expect(getGlRenderStateRuntime(state).clipForms).toEqual([]);
100
+ });
101
+ });