@flighthq/scene2d-gl 0.2.1-next.840.857425c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enableGlColorAdjustmentGuards.d.ts +4 -0
- package/dist/enableGlColorAdjustmentGuards.d.ts.map +1 -0
- package/dist/enableGlColorAdjustmentGuards.js +21 -0
- package/dist/enableGlColorAdjustmentGuards.js.map +1 -0
- package/dist/glBitmap.d.ts +4 -0
- package/dist/glBitmap.d.ts.map +1 -0
- package/dist/glBitmap.js +93 -0
- package/dist/glBitmap.js.map +1 -0
- package/dist/glCache.d.ts +30 -0
- package/dist/glCache.d.ts.map +1 -0
- package/dist/glCache.js +196 -0
- package/dist/glCache.js.map +1 -0
- package/dist/glClip.d.ts +3 -0
- package/dist/glClip.d.ts.map +1 -0
- package/dist/glClip.js +46 -0
- package/dist/glClip.js.map +1 -0
- package/dist/glClipContours.d.ts +4 -0
- package/dist/glClipContours.d.ts.map +1 -0
- package/dist/glClipContours.js +148 -0
- package/dist/glClipContours.js.map +1 -0
- package/dist/glClipRectangle.d.ts +4 -0
- package/dist/glClipRectangle.d.ts.map +1 -0
- package/dist/glClipRectangle.js +68 -0
- package/dist/glClipRectangle.js.map +1 -0
- package/dist/glColorAdjustment.d.ts +3 -0
- package/dist/glColorAdjustment.d.ts.map +1 -0
- package/dist/glColorAdjustment.js +361 -0
- package/dist/glColorAdjustment.js.map +1 -0
- package/dist/glDefaultMaterial.d.ts +4 -0
- package/dist/glDefaultMaterial.d.ts.map +1 -0
- package/dist/glDefaultMaterial.js +20 -0
- package/dist/glDefaultMaterial.js.map +1 -0
- package/dist/glNode2D.d.ts +5 -0
- package/dist/glNode2D.d.ts.map +1 -0
- package/dist/glNode2D.js +41 -0
- package/dist/glNode2D.js.map +1 -0
- package/dist/glNode2DRegistration.d.ts +25 -0
- package/dist/glNode2DRegistration.d.ts.map +1 -0
- package/dist/glNode2DRegistration.js +51 -0
- package/dist/glNode2DRegistration.js.map +1 -0
- package/dist/glParticleEmitter2D.d.ts +4 -0
- package/dist/glParticleEmitter2D.d.ts.map +1 -0
- package/dist/glParticleEmitter2D.js +263 -0
- package/dist/glParticleEmitter2D.js.map +1 -0
- package/dist/glQuadBatch.d.ts +3 -0
- package/dist/glQuadBatch.d.ts.map +1 -0
- package/dist/glQuadBatch.js +92 -0
- package/dist/glQuadBatch.js.map +1 -0
- package/dist/glRichText.d.ts +8 -0
- package/dist/glRichText.d.ts.map +1 -0
- package/dist/glRichText.js +161 -0
- package/dist/glRichText.js.map +1 -0
- package/dist/glScale9Mapper.d.ts +3 -0
- package/dist/glScale9Mapper.d.ts.map +1 -0
- package/dist/glScale9Mapper.js +37 -0
- package/dist/glScale9Mapper.js.map +1 -0
- package/dist/glScale9Shape.d.ts +9 -0
- package/dist/glScale9Shape.d.ts.map +1 -0
- package/dist/glScale9Shape.js +118 -0
- package/dist/glScale9Shape.js.map +1 -0
- package/dist/glShape.d.ts +4 -0
- package/dist/glShape.d.ts.map +1 -0
- package/dist/glShape.js +146 -0
- package/dist/glShape.js.map +1 -0
- package/dist/glShapeMesh.d.ts +5 -0
- package/dist/glShapeMesh.d.ts.map +1 -0
- package/dist/glShapeMesh.js +114 -0
- package/dist/glShapeMesh.js.map +1 -0
- package/dist/glSprite.d.ts +3 -0
- package/dist/glSprite.d.ts.map +1 -0
- package/dist/glSprite.js +28 -0
- package/dist/glSprite.js.map +1 -0
- package/dist/glSpriteBatch.d.ts +11 -0
- package/dist/glSpriteBatch.d.ts.map +1 -0
- package/dist/glSpriteBatch.js +277 -0
- package/dist/glSpriteBatch.js.map +1 -0
- package/dist/glSpriteRenderer.d.ts +3 -0
- package/dist/glSpriteRenderer.d.ts.map +1 -0
- package/dist/glSpriteRenderer.js +54 -0
- package/dist/glSpriteRenderer.js.map +1 -0
- package/dist/glTestHelper.d.ts +9 -0
- package/dist/glTestHelper.d.ts.map +1 -0
- package/dist/glTestHelper.js +17 -0
- package/dist/glTestHelper.js.map +1 -0
- package/dist/glTextInput.d.ts +4 -0
- package/dist/glTextInput.d.ts.map +1 -0
- package/dist/glTextInput.js +43 -0
- package/dist/glTextInput.js.map +1 -0
- package/dist/glTextLabel.d.ts +4 -0
- package/dist/glTextLabel.d.ts.map +1 -0
- package/dist/glTextLabel.js +141 -0
- package/dist/glTextLabel.js.map +1 -0
- package/dist/glTilemap.d.ts +3 -0
- package/dist/glTilemap.d.ts.map +1 -0
- package/dist/glTilemap.js +79 -0
- package/dist/glTilemap.js.map +1 -0
- package/dist/glVelocity.d.ts +22 -0
- package/dist/glVelocity.d.ts.map +1 -0
- package/dist/glVelocity.js +319 -0
- package/dist/glVelocity.js.map +1 -0
- package/dist/glVideo.d.ts +6 -0
- package/dist/glVideo.d.ts.map +1 -0
- package/dist/glVideo.js +62 -0
- package/dist/glVideo.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/moduleMockTestHelper.d.ts +15 -0
- package/dist/moduleMockTestHelper.d.ts.map +1 -0
- package/dist/moduleMockTestHelper.js +25 -0
- package/dist/moduleMockTestHelper.js.map +1 -0
- package/package.json +60 -0
- package/src/enableGlColorAdjustmentGuards.test.ts +76 -0
- package/src/glBitmap.test.ts +136 -0
- package/src/glCache.test.ts +234 -0
- package/src/glClip.test.ts +101 -0
- package/src/glClipContours.test.ts +61 -0
- package/src/glClipRectangle.test.ts +47 -0
- package/src/glColorAdjustment.test.ts +166 -0
- package/src/glDefaultMaterial.test.ts +19 -0
- package/src/glNode2D.test.ts +85 -0
- package/src/glNode2DRegistration.test.ts +132 -0
- package/src/glParticleEmitter2D.test.ts +122 -0
- package/src/glQuadBatch.test.ts +132 -0
- package/src/glRichText.test.ts +140 -0
- package/src/glScale9Mapper.test.ts +20 -0
- package/src/glScale9Shape.test.ts +90 -0
- package/src/glShape.test.ts +116 -0
- package/src/glShapeMesh.test.ts +164 -0
- package/src/glSprite.test.ts +79 -0
- package/src/glSpriteBatch.test.ts +249 -0
- package/src/glSpriteRenderer.test.ts +89 -0
- package/src/glTextInput.test.ts +51 -0
- package/src/glTextLabel.test.ts +156 -0
- package/src/glTilemap.test.ts +127 -0
- package/src/glVelocity.test.ts +156 -0
- package/src/glVideo.test.ts +98 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { RenderProxy2D } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { registerDefaultGlMaterial } from './glDefaultMaterial';
|
|
4
|
+
import { defaultGlQuadBatchRenderer } from './glQuadBatch';
|
|
5
|
+
import { flushGlSpriteBatch } from './glSpriteBatch';
|
|
6
|
+
import { createGlState } from './glTestHelper';
|
|
7
|
+
|
|
8
|
+
function makeAtlas() {
|
|
9
|
+
const img = document.createElement('img');
|
|
10
|
+
return {
|
|
11
|
+
image: { source: img, width: 64, height: 64 },
|
|
12
|
+
regions: [{ x: 0, y: 0, width: 32, height: 32 }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeQuadBatchNode(data: Record<string, unknown> = {}): RenderProxy2D {
|
|
17
|
+
return {
|
|
18
|
+
source: {
|
|
19
|
+
data: {
|
|
20
|
+
atlas: makeAtlas(),
|
|
21
|
+
instanceCount: 1,
|
|
22
|
+
ids: new Int32Array([0]),
|
|
23
|
+
transforms: new Float32Array([0, 0]),
|
|
24
|
+
transformType: 'vector2',
|
|
25
|
+
...data,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
blendMode: 0,
|
|
29
|
+
alpha: 1,
|
|
30
|
+
material: null,
|
|
31
|
+
materialData: null,
|
|
32
|
+
renderer: null,
|
|
33
|
+
traverseChildren: false,
|
|
34
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
35
|
+
} as unknown as RenderProxy2D;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('defaultGlQuadBatchRenderer', () => {
|
|
39
|
+
it('has a createData function', () => {
|
|
40
|
+
expect(typeof defaultGlQuadBatchRenderer.createData).toBe('function');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('has a submit function', () => {
|
|
44
|
+
expect(typeof defaultGlQuadBatchRenderer.submit).toBe('function');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('defaultGlQuadBatchRenderer.submit', () => {
|
|
49
|
+
it('returns early without drawing when atlas is null', () => {
|
|
50
|
+
const { state, gl } = createGlState();
|
|
51
|
+
registerDefaultGlMaterial(state);
|
|
52
|
+
defaultGlQuadBatchRenderer.submit(state, makeQuadBatchNode({ atlas: null }));
|
|
53
|
+
flushGlSpriteBatch(state as any);
|
|
54
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns early without drawing when atlas.image is null', () => {
|
|
58
|
+
const { state, gl } = createGlState();
|
|
59
|
+
registerDefaultGlMaterial(state);
|
|
60
|
+
defaultGlQuadBatchRenderer.submit(state, makeQuadBatchNode({ atlas: { image: null, regions: [] } }));
|
|
61
|
+
flushGlSpriteBatch(state as any);
|
|
62
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns early without drawing when atlas.image.source is null', () => {
|
|
66
|
+
const { state, gl } = createGlState();
|
|
67
|
+
registerDefaultGlMaterial(state);
|
|
68
|
+
defaultGlQuadBatchRenderer.submit(state, makeQuadBatchNode({ atlas: { image: { source: null }, regions: [] } }));
|
|
69
|
+
flushGlSpriteBatch(state as any);
|
|
70
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns early without drawing when instanceCount is 0', () => {
|
|
74
|
+
const { state, gl } = createGlState();
|
|
75
|
+
registerDefaultGlMaterial(state);
|
|
76
|
+
defaultGlQuadBatchRenderer.submit(state, makeQuadBatchNode({ instanceCount: 0 }));
|
|
77
|
+
flushGlSpriteBatch(state as any);
|
|
78
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('draws all valid instances in a single instanced call with vector2 transform type', () => {
|
|
82
|
+
const { state, gl } = createGlState();
|
|
83
|
+
registerDefaultGlMaterial(state);
|
|
84
|
+
defaultGlQuadBatchRenderer.submit(
|
|
85
|
+
state,
|
|
86
|
+
makeQuadBatchNode({
|
|
87
|
+
instanceCount: 2,
|
|
88
|
+
ids: new Int32Array([0, 0]),
|
|
89
|
+
transforms: new Float32Array([0, 0, 10, 20]),
|
|
90
|
+
transformType: 'vector2',
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
flushGlSpriteBatch(state as any);
|
|
94
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('excludes out-of-range ids from the instanced draw count', () => {
|
|
99
|
+
const { state, gl } = createGlState();
|
|
100
|
+
registerDefaultGlMaterial(state);
|
|
101
|
+
defaultGlQuadBatchRenderer.submit(
|
|
102
|
+
state,
|
|
103
|
+
makeQuadBatchNode({
|
|
104
|
+
instanceCount: 3,
|
|
105
|
+
ids: new Int32Array([0, 99, 0]),
|
|
106
|
+
transforms: new Float32Array([0, 0, 0, 0, 0, 0]),
|
|
107
|
+
transformType: 'vector2',
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
flushGlSpriteBatch(state as any);
|
|
111
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('draws all valid instances in a single instanced call with full matrix transform type', () => {
|
|
116
|
+
const { state, gl } = createGlState();
|
|
117
|
+
registerDefaultGlMaterial(state);
|
|
118
|
+
const transforms = new Float32Array([1, 0, 0, 1, 0, 0]);
|
|
119
|
+
defaultGlQuadBatchRenderer.submit(
|
|
120
|
+
state,
|
|
121
|
+
makeQuadBatchNode({
|
|
122
|
+
instanceCount: 1,
|
|
123
|
+
ids: new Int32Array([0]),
|
|
124
|
+
transforms,
|
|
125
|
+
transformType: 'matrix',
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
flushGlSpriteBatch(state as any);
|
|
129
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 1);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
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 multi-format spans', () => {
|
|
89
|
+
const { state, gl } = createGlState();
|
|
90
|
+
const renderProxy = makeRichTextNode();
|
|
91
|
+
const node = renderProxy.source as RichText;
|
|
92
|
+
node.data.text = 'BoldGreen';
|
|
93
|
+
node.data.textFormatRanges = [
|
|
94
|
+
{ start: 0, end: 4, format: { bold: true } },
|
|
95
|
+
{ start: 4, end: 9, format: { color: 0x00ff00 } },
|
|
96
|
+
];
|
|
97
|
+
drawGlRichText(state, renderProxy);
|
|
98
|
+
expect(gl.drawElements).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('draws field chrome even when text is empty', () => {
|
|
102
|
+
const { state, gl } = createGlState();
|
|
103
|
+
const renderProxy = makeRichTextNode();
|
|
104
|
+
(renderProxy.source as RichText).data.background = true;
|
|
105
|
+
drawGlRichText(state, renderProxy);
|
|
106
|
+
expect(gl.drawElements).toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('drawGlRichTextWithOverlay', () => {
|
|
111
|
+
it('runs an optional canvas overlay after layout', () => {
|
|
112
|
+
const { state } = createGlState();
|
|
113
|
+
const renderProxy = makeRichTextNode();
|
|
114
|
+
(renderProxy.source as RichText).data.text = 'hello';
|
|
115
|
+
const overlay = vi.fn();
|
|
116
|
+
|
|
117
|
+
drawGlRichTextWithOverlay(state, renderProxy, overlay);
|
|
118
|
+
|
|
119
|
+
expect(overlay).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('registerGlTextInputOverlay', () => {
|
|
124
|
+
it('invokes the registered overlay only for a RichText with an input slot', () => {
|
|
125
|
+
const overlay = vi.fn();
|
|
126
|
+
registerGlTextInputOverlay(overlay);
|
|
127
|
+
const { state } = createGlState();
|
|
128
|
+
|
|
129
|
+
const plain = makeRichTextNode();
|
|
130
|
+
(plain.source as RichText).data.text = 'x';
|
|
131
|
+
drawGlRichText(state, plain);
|
|
132
|
+
expect(overlay).not.toHaveBeenCalled();
|
|
133
|
+
|
|
134
|
+
const editable = makeRichTextNode();
|
|
135
|
+
(editable.source as RichText).data.text = 'x';
|
|
136
|
+
enableTextInput(editable.source as RichText);
|
|
137
|
+
drawGlRichText(state, editable);
|
|
138
|
+
expect(overlay).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -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,116 @@
|
|
|
1
|
+
import { createImageResource } from '@flighthq/image';
|
|
2
|
+
import type * as FlightNodeModule from '@flighthq/node';
|
|
3
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
4
|
+
import type { RenderProxy2D } from '@flighthq/types';
|
|
5
|
+
import { BatchFormat } from '@flighthq/types';
|
|
6
|
+
|
|
7
|
+
import { registerDefaultGlMaterial } from './glDefaultMaterial';
|
|
8
|
+
import type * as GlShapeModule from './glShape';
|
|
9
|
+
import { flushGlSpriteBatch } from './glSpriteBatch';
|
|
10
|
+
import { createGlState } from './glTestHelper';
|
|
11
|
+
import { scopeModuleMocks } from './moduleMockTestHelper';
|
|
12
|
+
|
|
13
|
+
// @flighthq/node's bounds/revision queries expect a real BoundsNode; these tests drive drawGlShape
|
|
14
|
+
// with lightweight fake proxies, so the two queries are stubbed. scopeModuleMocks scopes the stub to
|
|
15
|
+
// this file (registry reset before the mock applies, unmock + reset after), so under a shared
|
|
16
|
+
// (isolate:false) worker it never leaks into the many real consumers of these functions (node,
|
|
17
|
+
// interaction, shape, text) — and a sibling that pre-evaluated ./glShape still picks up the stub.
|
|
18
|
+
let defaultGlShapeRenderer: typeof GlShapeModule.defaultGlShapeRenderer;
|
|
19
|
+
let drawGlShape: typeof GlShapeModule.drawGlShape;
|
|
20
|
+
|
|
21
|
+
scopeModuleMocks(['@flighthq/node']);
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
vi.doMock('@flighthq/node', async (importOriginal) => ({
|
|
25
|
+
...(await importOriginal<typeof FlightNodeModule>()),
|
|
26
|
+
getNodeLocalBoundsRectangle: () => ({ x: 0, y: 0, width: 64, height: 48 }),
|
|
27
|
+
getNodeLocalContentRevision: (source: any) => source?.data?.version ?? 0,
|
|
28
|
+
}));
|
|
29
|
+
({ defaultGlShapeRenderer, drawGlShape } = await import('./glShape'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
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 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeShapeNode(data: Record<string, unknown> = {}, rendererData: unknown = null): RenderProxy2D {
|
|
41
|
+
return {
|
|
42
|
+
source: {
|
|
43
|
+
data: {
|
|
44
|
+
commands: [],
|
|
45
|
+
version: 0,
|
|
46
|
+
...data,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
rendererData: rendererData,
|
|
50
|
+
blendMode: 0,
|
|
51
|
+
alpha: 1,
|
|
52
|
+
material: null,
|
|
53
|
+
materialData: null,
|
|
54
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
55
|
+
} as unknown as RenderProxy2D;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('defaultGlShapeRenderer', () => {
|
|
59
|
+
it('declares BatchFormat.Quad', () => {
|
|
60
|
+
expect(defaultGlShapeRenderer.format).toBe(BatchFormat.Quad);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('has a createData function', () => {
|
|
64
|
+
expect(typeof defaultGlShapeRenderer.createData).toBe('function');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('has a submit function pointing to drawGlShape', () => {
|
|
68
|
+
expect(defaultGlShapeRenderer.submit).toBe(drawGlShape);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('drawGlShape', () => {
|
|
73
|
+
it('returns early without writing to batch when commands array is empty', () => {
|
|
74
|
+
const { state } = createGlState();
|
|
75
|
+
registerDefaultGlMaterial(state);
|
|
76
|
+
drawGlShape(state, makeShapeNode({ commands: [] }, makeShapeData()));
|
|
77
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns early without writing to batch when rendererData is null', () => {
|
|
81
|
+
const { state } = createGlState();
|
|
82
|
+
registerDefaultGlMaterial(state);
|
|
83
|
+
drawGlShape(state, makeShapeNode({ commands: [{}] }, null));
|
|
84
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns early without writing to batch when no material renderer is registered', () => {
|
|
88
|
+
const { state } = createGlState();
|
|
89
|
+
drawGlShape(state, makeShapeNode({ commands: [{}] }, makeShapeData()));
|
|
90
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('writes one instance to the sprite batch when shape has valid commands and bounds', () => {
|
|
94
|
+
const { state } = createGlState();
|
|
95
|
+
registerDefaultGlMaterial(state);
|
|
96
|
+
drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
|
|
97
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('draws via drawElementsInstanced after flush', () => {
|
|
101
|
+
const { state, gl } = createGlState();
|
|
102
|
+
registerDefaultGlMaterial(state);
|
|
103
|
+
drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
|
|
104
|
+
flushGlSpriteBatch(state);
|
|
105
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('writes correct size into instance data', () => {
|
|
109
|
+
const { state } = createGlState();
|
|
110
|
+
registerDefaultGlMaterial(state);
|
|
111
|
+
drawGlShape(state, makeShapeNode({ commands: [{}], version: 1 }, makeShapeData()));
|
|
112
|
+
const d = getGlRenderStateRuntime(state).spriteBatchInstanceData;
|
|
113
|
+
expect(d[6]).toBe(64); // width from mocked bounds
|
|
114
|
+
expect(d[7]).toBe(48); // height from mocked bounds
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
2
|
+
import type { ColorTransform, RenderProxy2D } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { enableGlColorAdjustment } from './glColorAdjustment';
|
|
5
|
+
import { drawGlShapeMeshBatch, drawGlShapeMeshes, ensureGlShapeMeshProgram } from './glShapeMesh';
|
|
6
|
+
import { createGlState } from './glTestHelper';
|
|
7
|
+
|
|
8
|
+
function makeProxy(overrides?: Partial<RenderProxy2D>): RenderProxy2D {
|
|
9
|
+
return {
|
|
10
|
+
alpha: 1,
|
|
11
|
+
blendMode: null,
|
|
12
|
+
colorTransform: null,
|
|
13
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
14
|
+
...overrides,
|
|
15
|
+
} as unknown as RenderProxy2D;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ct(
|
|
19
|
+
redMultiplier = 1,
|
|
20
|
+
greenMultiplier = 1,
|
|
21
|
+
blueMultiplier = 1,
|
|
22
|
+
alphaMultiplier = 1,
|
|
23
|
+
redOffset = 0,
|
|
24
|
+
greenOffset = 0,
|
|
25
|
+
blueOffset = 0,
|
|
26
|
+
alphaOffset = 0,
|
|
27
|
+
): ColorTransform {
|
|
28
|
+
return {
|
|
29
|
+
redMultiplier,
|
|
30
|
+
greenMultiplier,
|
|
31
|
+
blueMultiplier,
|
|
32
|
+
alphaMultiplier,
|
|
33
|
+
redOffset,
|
|
34
|
+
greenOffset,
|
|
35
|
+
blueOffset,
|
|
36
|
+
alphaOffset,
|
|
37
|
+
} as ColorTransform;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TRIANGLE = {
|
|
41
|
+
vertices: new Float32Array([0, 0, 10, 0, 0, 10]),
|
|
42
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
43
|
+
color: 0xff8040,
|
|
44
|
+
alpha: 1,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('drawGlShapeMeshBatch', () => {
|
|
48
|
+
it('runs onProgramBound after the matrix upload and before the draw loop', () => {
|
|
49
|
+
const { state, gl } = createGlState();
|
|
50
|
+
const calls: string[] = [];
|
|
51
|
+
(gl.uniformMatrix3fv as ReturnType<typeof vi.fn>).mockImplementation(() => calls.push('matrix'));
|
|
52
|
+
(gl.drawElements as ReturnType<typeof vi.fn>).mockImplementation(() => calls.push('draw'));
|
|
53
|
+
|
|
54
|
+
drawGlShapeMeshBatch(state, makeProxy(), [TRIANGLE], ensureGlShapeMeshProgram(state), () => calls.push('bound'));
|
|
55
|
+
|
|
56
|
+
expect(calls).toEqual(['matrix', 'bound', 'draw']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('is a no-op for an empty mesh list', () => {
|
|
60
|
+
const { state, gl } = createGlState();
|
|
61
|
+
drawGlShapeMeshBatch(state, makeProxy(), [], ensureGlShapeMeshProgram(state));
|
|
62
|
+
expect(gl.useProgram).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('drawGlShapeMeshes', () => {
|
|
67
|
+
it('binds the mesh program and draws each mesh', () => {
|
|
68
|
+
const { state, gl } = createGlState();
|
|
69
|
+
|
|
70
|
+
drawGlShapeMeshes(state, makeProxy(), [TRIANGLE]);
|
|
71
|
+
|
|
72
|
+
expect(gl.useProgram).toHaveBeenCalled();
|
|
73
|
+
expect(gl.drawElements).toHaveBeenCalledWith(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('records the mesh program as currentProgram so content draws re-bind', () => {
|
|
77
|
+
const { state } = createGlState();
|
|
78
|
+
expect(getGlRenderStateRuntime(state).currentProgram).toBeNull();
|
|
79
|
+
|
|
80
|
+
drawGlShapeMeshes(state, makeProxy(), [TRIANGLE]);
|
|
81
|
+
|
|
82
|
+
expect(getGlRenderStateRuntime(state).currentProgram).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uploads premultiplied color (color * alpha) for the standard blend', () => {
|
|
86
|
+
const { state, gl } = createGlState();
|
|
87
|
+
|
|
88
|
+
drawGlShapeMeshes(state, makeProxy(), [{ ...TRIANGLE, color: 0xffffff, alpha: 0.5 }]);
|
|
89
|
+
|
|
90
|
+
expect(gl.uniform4f).toHaveBeenCalledWith(expect.anything(), 0.5, 0.5, 0.5, 0.5);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('skips fully transparent meshes', () => {
|
|
94
|
+
const { state, gl } = createGlState();
|
|
95
|
+
|
|
96
|
+
drawGlShapeMeshes(state, makeProxy(), [{ ...TRIANGLE, alpha: 0 }]);
|
|
97
|
+
|
|
98
|
+
expect(gl.drawElements).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('is a no-op for an empty mesh list', () => {
|
|
102
|
+
const { state, gl } = createGlState();
|
|
103
|
+
|
|
104
|
+
drawGlShapeMeshes(state, makeProxy(), []);
|
|
105
|
+
|
|
106
|
+
expect(gl.useProgram).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('ignores a color transform when color adjustment is not enabled (default path pays nothing)', () => {
|
|
110
|
+
const { state } = createGlState();
|
|
111
|
+
|
|
112
|
+
drawGlShapeMeshes(state, makeProxy({ colorTransform: ct(0.5) }), [TRIANGLE]);
|
|
113
|
+
|
|
114
|
+
// The tinted program is compiled only through the opt-in fold; the base path never touches it.
|
|
115
|
+
expect(getGlRenderStateRuntime(state).shapeMeshColorTransformShader).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('tints solid-fill meshes through the fold with the same uniforms as the quad-batch path', () => {
|
|
119
|
+
const { state, gl } = createGlState();
|
|
120
|
+
enableGlColorAdjustment(state);
|
|
121
|
+
|
|
122
|
+
// White fill, half-brightness multiplier, +128 red offset — mirrors the Path-B uniform upload.
|
|
123
|
+
drawGlShapeMeshes(state, makeProxy({ colorTransform: ct(0.5, 0.5, 0.5, 1, 128, 0, 0, 0) }), [
|
|
124
|
+
{ ...TRIANGLE, color: 0xffffff, alpha: 1 },
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const shader = getGlRenderStateRuntime(state).shapeMeshColorTransformShader!;
|
|
128
|
+
expect(shader).toBeDefined();
|
|
129
|
+
// Multiplier uploaded verbatim; offsets normalized by 255 — identical to glColorAdjustment's
|
|
130
|
+
// bindGlSpriteBatchUniformColorTransform.
|
|
131
|
+
expect(gl.uniform4f).toHaveBeenCalledWith(shader.colorMultiplierLocation, 0.5, 0.5, 0.5, 1);
|
|
132
|
+
expect(gl.uniform4f).toHaveBeenCalledWith(shader.colorOffsetLocation, 128 / 255, 0, 0, 0);
|
|
133
|
+
// Flat mesh color still uploaded premultiplied (white, alpha 1); the shader un/re-premultiplies.
|
|
134
|
+
expect(gl.uniform4f).toHaveBeenCalledWith(shader.colorLocation, 1, 1, 1, 1);
|
|
135
|
+
expect(gl.drawElements).toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('falls back to the lean path through the fold when the node carries no transform', () => {
|
|
139
|
+
const { state } = createGlState();
|
|
140
|
+
enableGlColorAdjustment(state);
|
|
141
|
+
|
|
142
|
+
drawGlShapeMeshes(state, makeProxy({ colorTransform: null }), [TRIANGLE]);
|
|
143
|
+
|
|
144
|
+
// No transform → the fold is not consulted (gated on non-null), so no tint shader is compiled.
|
|
145
|
+
expect(getGlRenderStateRuntime(state).shapeMeshColorTransformShader).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('ensureGlShapeMeshProgram', () => {
|
|
150
|
+
it('compiles once and caches the binding per context', () => {
|
|
151
|
+
const { state } = createGlState();
|
|
152
|
+
const first = ensureGlShapeMeshProgram(state);
|
|
153
|
+
const second = ensureGlShapeMeshProgram(state);
|
|
154
|
+
expect(second).toBe(first);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('exposes shared vertex and index buffers for the draw driver', () => {
|
|
158
|
+
const { state } = createGlState();
|
|
159
|
+
const binding = ensureGlShapeMeshProgram(state);
|
|
160
|
+
expect(binding.vertexBuffer).toBeDefined();
|
|
161
|
+
expect(binding.indexBuffer).toBeDefined();
|
|
162
|
+
expect(binding.vertexBuffer).not.toBe(binding.indexBuffer);
|
|
163
|
+
});
|
|
164
|
+
});
|