@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.
- package/dist/glBitmap.d.ts +4 -0
- package/dist/glBitmap.d.ts.map +1 -0
- package/dist/glBitmap.js +106 -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 +185 -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/glColorTransformMaterial.d.ts +4 -0
- package/dist/glColorTransformMaterial.d.ts.map +1 -0
- package/dist/glColorTransformMaterial.js +116 -0
- package/dist/glColorTransformMaterial.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/glDisplayObject.d.ts +5 -0
- package/dist/glDisplayObject.d.ts.map +1 -0
- package/dist/glDisplayObject.js +41 -0
- package/dist/glDisplayObject.js.map +1 -0
- package/dist/glDisplayObjectRegistration.d.ts +25 -0
- package/dist/glDisplayObjectRegistration.d.ts.map +1 -0
- package/dist/glDisplayObjectRegistration.js +51 -0
- package/dist/glDisplayObjectRegistration.js.map +1 -0
- package/dist/glMaterials.d.ts +4 -0
- package/dist/glMaterials.d.ts.map +1 -0
- package/dist/glMaterials.js +97 -0
- package/dist/glMaterials.js.map +1 -0
- package/dist/glParticleEmitter.d.ts +4 -0
- package/dist/glParticleEmitter.d.ts.map +1 -0
- package/dist/glParticleEmitter.js +258 -0
- package/dist/glParticleEmitter.js.map +1 -0
- package/dist/glQuadBatch.d.ts +3 -0
- package/dist/glQuadBatch.d.ts.map +1 -0
- package/dist/glQuadBatch.js +88 -0
- package/dist/glQuadBatch.js.map +1 -0
- package/dist/glRichText.d.ts +9 -0
- package/dist/glRichText.d.ts.map +1 -0
- package/dist/glRichText.js +149 -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 +125 -0
- package/dist/glShape.js.map +1 -0
- package/dist/glShapeMesh.d.ts +9 -0
- package/dist/glShapeMesh.d.ts.map +1 -0
- package/dist/glShapeMesh.js +90 -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 +241 -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 +50 -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 +130 -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 +74 -0
- package/dist/glTilemap.js.map +1 -0
- package/dist/glUniformColorTransformMaterial.d.ts +4 -0
- package/dist/glUniformColorTransformMaterial.d.ts.map +1 -0
- package/dist/glUniformColorTransformMaterial.js +56 -0
- package/dist/glUniformColorTransformMaterial.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 +60 -0
- package/dist/glVideo.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -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 +51 -0
- package/src/glBitmap.test.ts +128 -0
- package/src/glCache.test.ts +230 -0
- package/src/glClip.test.ts +101 -0
- package/src/glClipContours.test.ts +61 -0
- package/src/glClipRectangle.test.ts +47 -0
- package/src/glColorTransformMaterial.test.ts +45 -0
- package/src/glDefaultMaterial.test.ts +19 -0
- package/src/glDisplayObject.test.ts +85 -0
- package/src/glDisplayObjectRegistration.test.ts +130 -0
- package/src/glMaterials.test.ts +91 -0
- package/src/glParticleEmitter.test.ts +98 -0
- package/src/glQuadBatch.test.ts +132 -0
- package/src/glRichText.test.ts +135 -0
- package/src/glScale9Mapper.test.ts +20 -0
- package/src/glScale9Shape.test.ts +90 -0
- package/src/glShape.test.ts +115 -0
- package/src/glShapeMesh.test.ts +64 -0
- package/src/glSprite.test.ts +79 -0
- package/src/glSpriteBatch.test.ts +153 -0
- package/src/glSpriteRenderer.test.ts +75 -0
- package/src/glTextInput.test.ts +51 -0
- package/src/glTextLabel.test.ts +151 -0
- package/src/glTilemap.test.ts +127 -0
- package/src/glUniformColorTransformMaterial.test.ts +24 -0
- package/src/glVelocity.test.ts +155 -0
- package/src/glVideo.test.ts +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export * from './glBitmap';
|
|
2
|
+
export * from './glCache';
|
|
3
|
+
export * from './glClip';
|
|
4
|
+
export * from './glClipContours';
|
|
5
|
+
export * from './glClipRectangle';
|
|
6
|
+
export * from './glColorTransformMaterial';
|
|
7
|
+
export * from './glDefaultMaterial';
|
|
8
|
+
export * from './glDisplayObject';
|
|
9
|
+
export * from './glDisplayObjectRegistration';
|
|
10
|
+
export * from './glMaterials';
|
|
11
|
+
export * from './glParticleEmitter';
|
|
12
|
+
export * from './glQuadBatch';
|
|
13
|
+
export * from './glRichText';
|
|
14
|
+
export * from './glScale9Mapper';
|
|
15
|
+
export * from './glScale9Shape';
|
|
16
|
+
export * from './glShape';
|
|
17
|
+
export * from './glShapeMesh';
|
|
18
|
+
export * from './glSprite';
|
|
19
|
+
export * from './glSpriteBatch';
|
|
20
|
+
export * from './glSpriteRenderer';
|
|
21
|
+
export * from './glTextInput';
|
|
22
|
+
export * from './glTextLabel';
|
|
23
|
+
export * from './glTilemap';
|
|
24
|
+
export * from './glUniformColorTransformMaterial';
|
|
25
|
+
export * from './glVelocity';
|
|
26
|
+
export * from './glVideo';
|
|
27
|
+
// Re-export shape commands from canvas (shapes deferred to canvas for now)
|
|
28
|
+
export { defaultCanvasBeginBitmapFill as defaultGlBeginBitmapFill, defaultCanvasBeginFill as defaultGlBeginFill, defaultCanvasBeginGradientFill as defaultGlBeginGradientFill, defaultCanvasCubicCurveTo as defaultGlCubicCurveTo, defaultCanvasCurveTo as defaultGlCurveTo, defaultCanvasDrawCircle as defaultGlDrawCircle, defaultCanvasDrawEllipse as defaultGlDrawEllipse, defaultCanvasDrawRectangle as defaultGlDrawRectangle, defaultCanvasDrawRoundRectangle as defaultGlDrawRoundRectangle, defaultCanvasEndFill as defaultGlEndFill, defaultCanvasLineStyle as defaultGlLineStyle, defaultCanvasLineTo as defaultGlLineTo, defaultCanvasMoveTo as defaultGlMoveTo, defaultCanvasShapeCommands as defaultGlShapeCommands, registerCanvasShapeCommands as registerGlShapeCommands, } from '@flighthq/displayobject-canvas';
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,mCAAmC,CAAC;AAClD,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAE1B,2EAA2E;AAC3E,OAAO,EACL,4BAA4B,IAAI,wBAAwB,EACxD,sBAAsB,IAAI,kBAAkB,EAC5C,8BAA8B,IAAI,0BAA0B,EAC5D,yBAAyB,IAAI,qBAAqB,EAClD,oBAAoB,IAAI,gBAAgB,EACxC,uBAAuB,IAAI,mBAAmB,EAC9C,wBAAwB,IAAI,oBAAoB,EAChD,0BAA0B,IAAI,sBAAsB,EACpD,+BAA+B,IAAI,2BAA2B,EAC9D,oBAAoB,IAAI,gBAAgB,EACxC,sBAAsB,IAAI,kBAAkB,EAC5C,mBAAmB,IAAI,eAAe,EACtC,mBAAmB,IAAI,eAAe,EACtC,0BAA0B,IAAI,sBAAsB,EACpD,2BAA2B,IAAI,uBAAuB,GACvD,MAAM,gCAAgC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes vi.doMock-based module mocking safe under the shared (isolate:false) worker the root vitest
|
|
3
|
+
* config uses. Call once at the top of a test file with every module id the file mocks, then apply
|
|
4
|
+
* the mocks and dynamically import the subject inside a beforeAll — both must stay in the test file
|
|
5
|
+
* so relative ids resolve against it. (This helper is colocated in the same package src, so the
|
|
6
|
+
* doUnmock ids passed here resolve to the same modules the test's doMock ids do.)
|
|
7
|
+
*
|
|
8
|
+
* It brackets the file with two vi.resetModules() calls. The first is registered here so it runs
|
|
9
|
+
* before the file's own beforeAll applies the mocks: it forces a subject a sibling already evaluated
|
|
10
|
+
* against the real modules to re-evaluate under the mocks (otherwise the dynamic import returns the
|
|
11
|
+
* cached, real-bound copy). The second, after unmocking, drops the mocked instances so a later file
|
|
12
|
+
* re-imports the real modules instead of the mock left in the shared registry.
|
|
13
|
+
*/
|
|
14
|
+
export declare function scopeModuleMocks(mockedIds: readonly string[]): void;
|
|
15
|
+
//# sourceMappingURL=moduleMockTestHelper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moduleMockTestHelper.d.ts","sourceRoot":"","sources":["../src/moduleMockTestHelper.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAQnE"}
|
|
@@ -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,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/displayobject-gl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/displayobject": "0.1.0",
|
|
31
|
+
"@flighthq/displayobject-canvas": "0.1.0",
|
|
32
|
+
"@flighthq/geometry": "0.1.0",
|
|
33
|
+
"@flighthq/materials": "0.1.0",
|
|
34
|
+
"@flighthq/node": "0.1.0",
|
|
35
|
+
"@flighthq/path": "0.1.0",
|
|
36
|
+
"@flighthq/render": "0.1.0",
|
|
37
|
+
"@flighthq/render-gl": "0.1.0",
|
|
38
|
+
"@flighthq/shape": "0.1.0",
|
|
39
|
+
"@flighthq/text": "0.1.0",
|
|
40
|
+
"@flighthq/textinput": "0.1.0",
|
|
41
|
+
"@flighthq/textlayout": "0.1.0",
|
|
42
|
+
"@flighthq/types": "0.1.0",
|
|
43
|
+
"@flighthq/velocity": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@flighthq/sprite": "*",
|
|
47
|
+
"typescript": "^5.3.0"
|
|
48
|
+
},
|
|
49
|
+
"description": "WebGL2 display-object renderers (bitmap, shape, sprite, text, tilemap, particles)",
|
|
50
|
+
"sideEffects": false
|
|
51
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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) {
|
|
27
|
+
return { source, width, height };
|
|
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 image.source is null', () => {
|
|
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 to the sprite batch when image is valid', () => {
|
|
64
|
+
const { state } = createGlState();
|
|
65
|
+
registerDefaultGlMaterial(state);
|
|
66
|
+
const img = document.createElement('img');
|
|
67
|
+
drawGlBitmap(state, makeRenderProxy(makeImageResource(img, 32, 32)));
|
|
68
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('draws via drawElementsInstanced after flush', () => {
|
|
72
|
+
const { state, gl } = createGlState();
|
|
73
|
+
registerDefaultGlMaterial(state);
|
|
74
|
+
const img = document.createElement('img');
|
|
75
|
+
drawGlBitmap(state, makeRenderProxy(makeImageResource(img)));
|
|
76
|
+
flushGlSpriteBatch(state as any);
|
|
77
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('writes correct transform and size into instance data', () => {
|
|
81
|
+
const { state } = createGlState();
|
|
82
|
+
registerDefaultGlMaterial(state);
|
|
83
|
+
const img = document.createElement('img');
|
|
84
|
+
drawGlBitmap(state, makeRenderProxy(makeImageResource(img, 64, 48)));
|
|
85
|
+
const d = getGlRenderStateRuntime(state).spriteBatchInstanceData;
|
|
86
|
+
expect(d[0]).toBe(1); // a
|
|
87
|
+
expect(d[1]).toBe(0); // b
|
|
88
|
+
expect(d[2]).toBe(0); // c
|
|
89
|
+
expect(d[3]).toBe(1); // d
|
|
90
|
+
expect(d[4]).toBe(10); // tx
|
|
91
|
+
expect(d[5]).toBe(20); // ty
|
|
92
|
+
expect(d[6]).toBe(64); // width
|
|
93
|
+
expect(d[7]).toBe(48); // height
|
|
94
|
+
expect(d[8]).toBeCloseTo(0); // u0
|
|
95
|
+
expect(d[9]).toBeCloseTo(0); // v0
|
|
96
|
+
expect(d[10]).toBeCloseTo(1); // u1
|
|
97
|
+
expect(d[11]).toBeCloseTo(1); // v1
|
|
98
|
+
expect(d[12]).toBe(1); // alpha
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('writes source-rectangle UVs when sourceRectangle is set', () => {
|
|
102
|
+
const { state } = createGlState();
|
|
103
|
+
registerDefaultGlMaterial(state);
|
|
104
|
+
const img = document.createElement('img');
|
|
105
|
+
const proxy = {
|
|
106
|
+
...makeRenderProxy({
|
|
107
|
+
source: img,
|
|
108
|
+
width: 128,
|
|
109
|
+
height: 64,
|
|
110
|
+
sourceRectangle: { x: 16, y: 8, width: 32, height: 16 },
|
|
111
|
+
}),
|
|
112
|
+
source: {
|
|
113
|
+
data: {
|
|
114
|
+
image: { source: img, width: 128, height: 64 },
|
|
115
|
+
sourceRectangle: { x: 16, y: 8, width: 32, height: 16 },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
} as unknown as RenderProxy2D;
|
|
119
|
+
drawGlBitmap(state, proxy);
|
|
120
|
+
const d = getGlRenderStateRuntime(state).spriteBatchInstanceData;
|
|
121
|
+
expect(d[6]).toBe(32); // width = sr.width
|
|
122
|
+
expect(d[7]).toBe(16); // height = sr.height
|
|
123
|
+
expect(d[8]).toBeCloseTo(16 / 128); // u0
|
|
124
|
+
expect(d[9]).toBeCloseTo(8 / 64); // v0
|
|
125
|
+
expect(d[10]).toBeCloseTo(48 / 128); // u1
|
|
126
|
+
expect(d[11]).toBeCloseTo(24 / 64); // v1
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
3
|
+
import { createRenderCache, createRenderState, RenderCacheKind, useRenderCache } from '@flighthq/render';
|
|
4
|
+
import type * as GlRenderGlModule from '@flighthq/render-gl';
|
|
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 GlDisplayObjectModule from './glDisplayObject';
|
|
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 ./glDisplayObject 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 / displayobject-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 renderGlDisplayObject: typeof GlDisplayObjectModule.renderGlDisplayObject;
|
|
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', './glDisplayObject']);
|
|
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
|
+
beginGlRenderTarget: vi.fn(),
|
|
51
|
+
createGlRenderTarget: vi.fn((_state: unknown, descriptor: { width: number; height: number }): GlRenderTarget => {
|
|
52
|
+
const texture = {} as WebGLTexture;
|
|
53
|
+
return {
|
|
54
|
+
framebuffer: {} as WebGLFramebuffer,
|
|
55
|
+
resolveFramebuffer: null,
|
|
56
|
+
texture,
|
|
57
|
+
textures: [texture],
|
|
58
|
+
depthTexture: null,
|
|
59
|
+
colorRenderbuffers: [],
|
|
60
|
+
depthStencilRenderbuffer: null,
|
|
61
|
+
format: 'rgba8',
|
|
62
|
+
sampleCount: 1,
|
|
63
|
+
width: descriptor.width,
|
|
64
|
+
height: descriptor.height,
|
|
65
|
+
};
|
|
66
|
+
}),
|
|
67
|
+
destroyGlRenderTarget: vi.fn(),
|
|
68
|
+
drawGlRenderTargetResult: vi.fn(),
|
|
69
|
+
endGlRenderTarget: vi.fn(),
|
|
70
|
+
resizeGlRenderTarget: vi.fn((_state: unknown, target: GlRenderTarget, width: number, height: number) => {
|
|
71
|
+
target.width = width;
|
|
72
|
+
target.height = height;
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
vi.doMock('./glDisplayObject', async (importOriginal) => {
|
|
77
|
+
const actual = await importOriginal<typeof GlDisplayObjectModule>();
|
|
78
|
+
return { ...actual, renderGlDisplayObject: vi.fn() };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
({ createGlRenderStateRuntime, getGlRenderStateRuntime, destroyGlRenderTarget, drawGlRenderTargetResult } =
|
|
82
|
+
await import('@flighthq/render-gl'));
|
|
83
|
+
({ flushGlSpriteBatch } = await import('./glSpriteBatch'));
|
|
84
|
+
({ renderGlDisplayObject } = await import('./glDisplayObject'));
|
|
85
|
+
({
|
|
86
|
+
createGlCacheState,
|
|
87
|
+
defaultGlRenderCacheRenderer,
|
|
88
|
+
enableGlRenderCache,
|
|
89
|
+
ensureGlRenderCacheTarget,
|
|
90
|
+
getGlRenderCacheTarget,
|
|
91
|
+
refreshGlRenderCache,
|
|
92
|
+
releaseGlRenderCache,
|
|
93
|
+
} = await import('./glCache'));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
function fakeScreen(options = {}): GlRenderState {
|
|
97
|
+
const state = createRenderState(options) as unknown as GlRenderState;
|
|
98
|
+
(state as any).gl = { clear: vi.fn(), clearColor: vi.fn(), COLOR_BUFFER_BIT: 0x4000 };
|
|
99
|
+
state[EntityRuntimeKey] = createGlRenderStateRuntime();
|
|
100
|
+
return state;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function makeCacheNode(source: unknown): any {
|
|
104
|
+
return { source, kind: RenderCacheKind, transform2D: createMatrix(), alpha: 1, blendMode: null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe('createGlCacheState', () => {
|
|
108
|
+
it('copies renderers and shares the GL context but keeps its own node map', () => {
|
|
109
|
+
const screen = fakeScreen();
|
|
110
|
+
enableGlRenderCache(screen);
|
|
111
|
+
const cacheState = createGlCacheState(screen);
|
|
112
|
+
expect(getGlRenderStateRuntime(cacheState).rendererMap.get(RenderCacheKind)).toBe(defaultGlRenderCacheRenderer);
|
|
113
|
+
expect((cacheState as any).gl).toBe((screen as any).gl);
|
|
114
|
+
expect(getGlRenderStateRuntime(cacheState).renderProxyMap).not.toBe(getGlRenderStateRuntime(screen).renderProxyMap);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('defaultGlRenderCacheRenderer', () => {
|
|
119
|
+
it('does nothing when no cache is attached to the source', () => {
|
|
120
|
+
const state = fakeScreen();
|
|
121
|
+
defaultGlRenderCacheRenderer.submit(state, makeCacheNode(createDisplayObject()));
|
|
122
|
+
expect(drawGlRenderTargetResult).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('composites the cache target attached to the source node', () => {
|
|
126
|
+
const state = fakeScreen();
|
|
127
|
+
const obj = createDisplayObject();
|
|
128
|
+
const cache = createRenderCache();
|
|
129
|
+
useRenderCache(state, obj, cache);
|
|
130
|
+
const target = ensureGlRenderCacheTarget(state, cache, 16, 16);
|
|
131
|
+
defaultGlRenderCacheRenderer.submit(state, makeCacheNode(obj));
|
|
132
|
+
expect(drawGlRenderTargetResult).toHaveBeenCalledWith(state, expect.anything(), target, expect.anything());
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('flushes pending batched geometry before the immediate composite', () => {
|
|
136
|
+
const state = fakeScreen();
|
|
137
|
+
const obj = createDisplayObject();
|
|
138
|
+
const cache = createRenderCache();
|
|
139
|
+
useRenderCache(state, obj, cache);
|
|
140
|
+
ensureGlRenderCacheTarget(state, cache, 16, 16);
|
|
141
|
+
defaultGlRenderCacheRenderer.submit(state, makeCacheNode(obj));
|
|
142
|
+
// The composite draws an immediate quad outside the sprite batch; geometry submitted earlier in
|
|
143
|
+
// the walk must be drained first, or it replays after the cache result (a doubled image).
|
|
144
|
+
expect(flushGlSpriteBatch).toHaveBeenCalledWith(state);
|
|
145
|
+
expect((flushGlSpriteBatch as any).mock.invocationCallOrder[0]).toBeLessThan(
|
|
146
|
+
(drawGlRenderTargetResult as any).mock.invocationCallOrder[0],
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('enableGlRenderCache', () => {
|
|
152
|
+
it('registers the renderer for the render cache kind', () => {
|
|
153
|
+
const state = fakeScreen();
|
|
154
|
+
enableGlRenderCache(state);
|
|
155
|
+
expect(getGlRenderStateRuntime(state).rendererMap.get(RenderCacheKind)).toBe(defaultGlRenderCacheRenderer);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('ensureGlRenderCacheTarget', () => {
|
|
160
|
+
it('creates a target sized to the request', () => {
|
|
161
|
+
const state = fakeScreen();
|
|
162
|
+
const target = ensureGlRenderCacheTarget(state, createRenderCache(), 64, 32);
|
|
163
|
+
expect(target.width).toBe(64);
|
|
164
|
+
expect(target.height).toBe(32);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('reuses and resizes the same target on subsequent calls', () => {
|
|
168
|
+
const state = fakeScreen();
|
|
169
|
+
const cache = createRenderCache();
|
|
170
|
+
const first = ensureGlRenderCacheTarget(state, cache, 64, 32);
|
|
171
|
+
const second = ensureGlRenderCacheTarget(state, cache, 16, 16);
|
|
172
|
+
expect(second).toBe(first);
|
|
173
|
+
expect(second.width).toBe(16);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('keeps targets isolated per state for the same handle', () => {
|
|
177
|
+
const stateA = fakeScreen();
|
|
178
|
+
const stateB = fakeScreen();
|
|
179
|
+
const cache = createRenderCache();
|
|
180
|
+
expect(ensureGlRenderCacheTarget(stateA, cache, 8, 8)).not.toBe(ensureGlRenderCacheTarget(stateB, cache, 8, 8));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('getGlRenderCacheTarget', () => {
|
|
185
|
+
it('returns null before a target is allocated', () => {
|
|
186
|
+
expect(getGlRenderCacheTarget(fakeScreen(), createRenderCache())).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns the allocated target', () => {
|
|
190
|
+
const state = fakeScreen();
|
|
191
|
+
const cache = createRenderCache();
|
|
192
|
+
const target = ensureGlRenderCacheTarget(state, cache, 8, 8);
|
|
193
|
+
expect(getGlRenderCacheTarget(state, cache)).toBe(target);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('refreshGlRenderCache', () => {
|
|
198
|
+
it('bakes on the first call and allocates the target on the screen state', () => {
|
|
199
|
+
const screen = fakeScreen();
|
|
200
|
+
const cacheState = createGlCacheState(screen);
|
|
201
|
+
const cache = createRenderCache();
|
|
202
|
+
const obj = createDisplayObject();
|
|
203
|
+
const rebaked = refreshGlRenderCache(cacheState, cache, obj, { padding: 5 });
|
|
204
|
+
expect(rebaked).toBe(true);
|
|
205
|
+
expect(renderGlDisplayObject).toHaveBeenCalled();
|
|
206
|
+
const target = getGlRenderCacheTarget(screen, cache);
|
|
207
|
+
expect(target).not.toBeNull();
|
|
208
|
+
expect(target!.width).toBe(10);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('skips the bake under requiresInvalidation when nothing changed', () => {
|
|
212
|
+
const screen = fakeScreen({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
213
|
+
const cacheState = createGlCacheState(screen);
|
|
214
|
+
const cache = createRenderCache();
|
|
215
|
+
const obj = createDisplayObject();
|
|
216
|
+
refreshGlRenderCache(cacheState, cache, obj, { padding: 5 });
|
|
217
|
+
expect(refreshGlRenderCache(cacheState, cache, obj, { padding: 5 })).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('releaseGlRenderCache', () => {
|
|
222
|
+
it('destroys and drops the target for the cache', () => {
|
|
223
|
+
const state = fakeScreen();
|
|
224
|
+
const cache = createRenderCache();
|
|
225
|
+
const target = ensureGlRenderCacheTarget(state, cache, 8, 8);
|
|
226
|
+
releaseGlRenderCache(state, cache);
|
|
227
|
+
expect(destroyGlRenderTarget).toHaveBeenCalledWith(state, target);
|
|
228
|
+
expect(getGlRenderCacheTarget(state, cache)).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix, createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
4
|
+
import type { ClipRegion, DisplayObject, 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: DisplayObject, 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
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
3
|
+
|
|
4
|
+
import { popGlClipContours, pushGlClipContours } from './glClipContours';
|
|
5
|
+
import { createGlState } from './glTestHelper';
|
|
6
|
+
|
|
7
|
+
const SQUARE = [[0, 0, 50, 0, 50, 50, 0, 50]];
|
|
8
|
+
|
|
9
|
+
describe('popGlClipContours', () => {
|
|
10
|
+
it('decrements the stencil depth and disables the stencil test at depth 0', () => {
|
|
11
|
+
const { state, gl } = createGlState();
|
|
12
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
13
|
+
pushGlClipContours(state, SQUARE, 'nonZero', createMatrix());
|
|
14
|
+
expect(runtime.currentMaskDepth).toBe(1);
|
|
15
|
+
|
|
16
|
+
popGlClipContours(state);
|
|
17
|
+
|
|
18
|
+
expect(runtime.currentMaskDepth).toBe(0);
|
|
19
|
+
expect(gl.disable).toHaveBeenCalledWith(gl.STENCIL_TEST);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('pushGlClipContours', () => {
|
|
24
|
+
it('enables the stencil test and clears the buffer when opening the first clip', () => {
|
|
25
|
+
const { state, gl } = createGlState();
|
|
26
|
+
|
|
27
|
+
pushGlClipContours(state, SQUARE, 'nonZero', createMatrix());
|
|
28
|
+
|
|
29
|
+
expect(gl.enable).toHaveBeenCalledWith(gl.STENCIL_TEST);
|
|
30
|
+
expect(gl.clear).toHaveBeenCalledWith(gl.STENCIL_BUFFER_BIT);
|
|
31
|
+
expect(getGlRenderStateRuntime(state).currentMaskDepth).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('syncs state.currentProgram to the clip program so content draws re-bind their own program', () => {
|
|
35
|
+
const { state } = createGlState();
|
|
36
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
37
|
+
expect(runtime.currentProgram).toBeNull();
|
|
38
|
+
|
|
39
|
+
pushGlClipContours(state, SQUARE, 'nonZero', createMatrix());
|
|
40
|
+
|
|
41
|
+
// The clip binds its own position-only program; recording it forces the next content draw to detect
|
|
42
|
+
// the change and re-bind, instead of setting its uniforms against the clip program.
|
|
43
|
+
expect(runtime.currentProgram).not.toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('re-enables color writes after stencilling so gated content is drawn', () => {
|
|
47
|
+
const { state, gl } = createGlState();
|
|
48
|
+
|
|
49
|
+
pushGlClipContours(state, SQUARE, 'nonZero', createMatrix());
|
|
50
|
+
|
|
51
|
+
expect(gl.colorMask).toHaveBeenLastCalledWith(true, true, true, true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses the even-odd stencil op for an evenOdd winding', () => {
|
|
55
|
+
const { state, gl } = createGlState();
|
|
56
|
+
|
|
57
|
+
pushGlClipContours(state, SQUARE, 'evenOdd', createMatrix());
|
|
58
|
+
|
|
59
|
+
expect(gl.stencilOp).toHaveBeenCalledWith(gl.KEEP, gl.KEEP, gl.INVERT);
|
|
60
|
+
});
|
|
61
|
+
});
|