@flighthq/render 0.3.0-next.906.07cea63 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +6 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +6 -1
- package/dist/contract.js.map +1 -1
- package/dist/enableColorAdjustmentGuards.js +6 -6
- package/dist/enableColorAdjustmentGuards.js.map +1 -1
- package/dist/enableColorAdjustments.d.ts +4 -0
- package/dist/enableColorAdjustments.d.ts.map +1 -0
- package/dist/enableColorAdjustments.js +110 -0
- package/dist/enableColorAdjustments.js.map +1 -0
- package/dist/enableSceneRenderGuards.d.ts +22 -0
- package/dist/enableSceneRenderGuards.d.ts.map +1 -0
- package/dist/enableSceneRenderGuards.js +36 -0
- package/dist/enableSceneRenderGuards.js.map +1 -0
- package/dist/explainScene2DCoverage.d.ts +4 -0
- package/dist/explainScene2DCoverage.d.ts.map +1 -0
- package/dist/explainScene2DCoverage.js +57 -0
- package/dist/explainScene2DCoverage.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/renderProxy.d.ts.map +1 -1
- package/dist/renderProxy.js +34 -9
- package/dist/renderProxy.js.map +1 -1
- package/dist/renderRegistryGuards.d.ts +5 -0
- package/dist/renderRegistryGuards.d.ts.map +1 -0
- package/dist/renderRegistryGuards.js +87 -0
- package/dist/renderRegistryGuards.js.map +1 -0
- package/dist/renderRegistrySignals.d.ts +3 -0
- package/dist/renderRegistrySignals.d.ts.map +1 -0
- package/dist/renderRegistrySignals.js +19 -0
- package/dist/renderRegistrySignals.js.map +1 -0
- package/dist/renderState.d.ts +1 -0
- package/dist/renderState.d.ts.map +1 -1
- package/dist/renderState.js +26 -0
- package/dist/renderState.js.map +1 -1
- package/dist/renderTarget.d.ts +4 -2
- package/dist/renderTarget.d.ts.map +1 -1
- package/dist/renderTarget.js +52 -2
- package/dist/renderTarget.js.map +1 -1
- package/dist/renderTransform2d.d.ts.map +1 -1
- package/dist/renderTransform2d.js +4 -1
- package/dist/renderTransform2d.js.map +1 -1
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +16 -0
- package/dist/renderer.js.map +1 -1
- package/dist/scene2dWorkingColorSpace.d.ts +3 -0
- package/dist/scene2dWorkingColorSpace.d.ts.map +1 -0
- package/dist/scene2dWorkingColorSpace.js +19 -0
- package/dist/scene2dWorkingColorSpace.js.map +1 -0
- package/dist/sceneRender.d.ts +6 -2
- package/dist/sceneRender.d.ts.map +1 -1
- package/dist/sceneRender.js +47 -13
- package/dist/sceneRender.js.map +1 -1
- package/package.json +15 -12
- package/src/enableColorAdjustmentGuards.test.ts +6 -4
- package/src/enableColorAdjustments.test.ts +233 -0
- package/src/enableSceneRenderGuards.test.ts +96 -0
- package/src/explainScene2DCoverage.test.ts +113 -0
- package/src/renderProxy.test.ts +142 -11
- package/src/renderQueue.test.ts +1 -1
- package/src/renderRegistryGuards.test.ts +94 -0
- package/src/renderRegistrySignals.test.ts +15 -0
- package/src/renderState.test.ts +16 -1
- package/src/renderTarget.test.ts +113 -5
- package/src/renderTransform2d.test.ts +15 -1
- package/src/renderViewport.test.ts +1 -1
- package/src/renderer.test.ts +44 -1
- package/src/scene2dWorkingColorSpace.test.ts +11 -0
- package/src/sceneRender.test.ts +75 -2
- package/dist/renderColorScaleBias.d.ts +0 -3
- package/dist/renderColorScaleBias.d.ts.map +0 -1
- package/dist/renderColorScaleBias.js +0 -21
- package/dist/renderColorScaleBias.js.map +0 -1
- package/src/renderColorScaleBias.test.ts +0 -53
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/render",
|
|
3
|
-
"version": "0.3.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,23 +38,24 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/
|
|
40
|
-
"@flighthq/
|
|
41
|
-
"@flighthq/
|
|
42
|
-
"@flighthq/
|
|
43
|
-
"@flighthq/
|
|
44
|
-
"@flighthq/
|
|
45
|
-
"@flighthq/
|
|
46
|
-
"@flighthq/
|
|
47
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/adjustments": "0.3.0",
|
|
42
|
+
"@flighthq/color": "0.3.0",
|
|
43
|
+
"@flighthq/entity": "0.3.0",
|
|
44
|
+
"@flighthq/geometry": "0.3.0",
|
|
45
|
+
"@flighthq/log": "0.3.0",
|
|
46
|
+
"@flighthq/materials": "0.3.0",
|
|
47
|
+
"@flighthq/mesh": "0.3.0",
|
|
48
|
+
"@flighthq/node": "0.3.0",
|
|
49
|
+
"@flighthq/signals": "0.3.0",
|
|
50
|
+
"@flighthq/types": "0.3.0"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
|
-
"@flighthq/adjustments": "*",
|
|
51
53
|
"@flighthq/camera": "*",
|
|
52
54
|
"@flighthq/scene2d": "*",
|
|
53
55
|
"@flighthq/lighting": "*",
|
|
54
56
|
"@flighthq/scene3d": "*",
|
|
55
|
-
"@flighthq/
|
|
57
|
+
"@flighthq/quadbatch": "*",
|
|
58
|
+
"@flighthq/tilemap": "*",
|
|
56
59
|
"typescript": "^5.3.0"
|
|
57
60
|
},
|
|
58
61
|
"description": "Renderer registration, render state, render queue, and backend draw contracts",
|
|
@@ -5,9 +5,9 @@ import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
|
5
5
|
import type { Adjustment, Renderable } from '@flighthq/types/contract';
|
|
6
6
|
|
|
7
7
|
import { areColorAdjustmentGuardsEnabled, enableColorAdjustmentGuards } from './enableColorAdjustmentGuards';
|
|
8
|
-
import {
|
|
8
|
+
import { enableColorAdjustments } from './enableColorAdjustments';
|
|
9
9
|
import { createRenderProxy } from './renderProxy';
|
|
10
|
-
import { createRenderState } from './renderState';
|
|
10
|
+
import { createRenderState, getRenderStateRuntime } from './renderState';
|
|
11
11
|
|
|
12
12
|
describe('areColorAdjustmentGuardsEnabled', () => {
|
|
13
13
|
it('reports false until guards are installed, then true', () => {
|
|
@@ -22,6 +22,7 @@ describe('enableColorAdjustmentGuards', () => {
|
|
|
22
22
|
it('warns once when a node carries a non-inline-able non-matrix adjustment', () => {
|
|
23
23
|
const state = createRenderState();
|
|
24
24
|
enableColorAdjustmentGuards(state);
|
|
25
|
+
enableColorAdjustments(state);
|
|
25
26
|
const node = createDisplayObject();
|
|
26
27
|
const lut: Adjustment = { kind: 'acme.Lut' };
|
|
27
28
|
setNodeColorAdjustments(node, [lut]);
|
|
@@ -29,7 +30,7 @@ describe('enableColorAdjustmentGuards', () => {
|
|
|
29
30
|
const sink = createMemoryLogSink(8);
|
|
30
31
|
addLogSink(sink.sink);
|
|
31
32
|
try {
|
|
32
|
-
|
|
33
|
+
getRenderStateRuntime(state).colorAdjustmentResolver!(state, data);
|
|
33
34
|
const entries = getMemoryLogSinkEntries(sink);
|
|
34
35
|
expect(entries.length).toBe(1);
|
|
35
36
|
expect(String((entries[0].data as Record<string, unknown>).message)).toContain('not inline-able');
|
|
@@ -41,13 +42,14 @@ describe('enableColorAdjustmentGuards', () => {
|
|
|
41
42
|
it('stays silent for an affine (inline-able) color-adjustment stack', () => {
|
|
42
43
|
const state = createRenderState();
|
|
43
44
|
enableColorAdjustmentGuards(state);
|
|
45
|
+
enableColorAdjustments(state);
|
|
44
46
|
const node = createDisplayObject();
|
|
45
47
|
setNodeColorAdjustments(node, [createTintAdjustment(0x7fffffff)]);
|
|
46
48
|
const data = createRenderProxy(state, node as unknown as Renderable);
|
|
47
49
|
const sink = createMemoryLogSink(8);
|
|
48
50
|
addLogSink(sink.sink);
|
|
49
51
|
try {
|
|
50
|
-
|
|
52
|
+
getRenderStateRuntime(state).colorAdjustmentResolver!(state, data);
|
|
51
53
|
expect(getMemoryLogSinkEntries(sink).length).toBe(0);
|
|
52
54
|
} finally {
|
|
53
55
|
removeLogSink(sink.sink);
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyColorMatrixToColor,
|
|
3
|
+
createColorMatrixAdjustment,
|
|
4
|
+
createColorScaleBiasAdjustment,
|
|
5
|
+
createTintAdjustment,
|
|
6
|
+
} from '@flighthq/adjustments/contract';
|
|
7
|
+
import { addNodeChild, setNodeColorAdjustments } from '@flighthq/node/contract';
|
|
8
|
+
import { createDisplayObject, getNode2DRuntime } from '@flighthq/scene2d/contract';
|
|
9
|
+
import type { Renderable, RenderProxy, RenderState } from '@flighthq/types/contract';
|
|
10
|
+
|
|
11
|
+
import { areColorAdjustmentsEnabled, enableColorAdjustments } from './enableColorAdjustments';
|
|
12
|
+
import { createRenderProxy, getRenderProxy2D, prepareScene2DRender } from './renderProxy';
|
|
13
|
+
import { createRenderState, getRenderStateRuntime } from './renderState';
|
|
14
|
+
|
|
15
|
+
describe('areColorAdjustmentsEnabled', () => {
|
|
16
|
+
it('reports false until accumulation is installed, then true', () => {
|
|
17
|
+
const state = createRenderState();
|
|
18
|
+
expect(areColorAdjustmentsEnabled(state)).toBe(false);
|
|
19
|
+
enableColorAdjustments(state);
|
|
20
|
+
expect(areColorAdjustmentsEnabled(state)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('enableColorAdjustments', () => {
|
|
25
|
+
it('leaves color adjustments unresolved until the state opts in', () => {
|
|
26
|
+
const state = createRenderState();
|
|
27
|
+
const node = createDisplayObject();
|
|
28
|
+
setNodeColorAdjustments(node, [createTintAdjustment(0x7f0000ff)]);
|
|
29
|
+
|
|
30
|
+
prepareScene2DRender(state, node);
|
|
31
|
+
|
|
32
|
+
expect(getRenderProxy2D(state, node)?.colorScaleBias).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('resolves a node color-adjustment stack onto the render node as an affine ColorScaleBias', () => {
|
|
36
|
+
const state = createEnabledRenderState();
|
|
37
|
+
const node = createDisplayObject();
|
|
38
|
+
setNodeColorAdjustments(node, [createTintAdjustment(0x7f0000ff)]);
|
|
39
|
+
const data = createRenderProxy(state, node as unknown as Renderable);
|
|
40
|
+
resolveColorAdjustments(state, data);
|
|
41
|
+
expect(data.colorScaleBias).not.toBeNull();
|
|
42
|
+
expect(data.colorScaleBias!.redScale).toBeCloseTo(0x7f / 255);
|
|
43
|
+
expect(data.colorScaleBias!.greenScale).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('resolves channel mixing onto the render node as a complete matrix', () => {
|
|
47
|
+
const state = createEnabledRenderState();
|
|
48
|
+
const node = createDisplayObject();
|
|
49
|
+
const matrix = [1, 0.5, 0, 0, 0.1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
|
|
50
|
+
setNodeColorAdjustments(node, [createColorMatrixAdjustment(matrix)]);
|
|
51
|
+
const data = createRenderProxy(state, node as unknown as Renderable);
|
|
52
|
+
resolveColorAdjustments(state, data);
|
|
53
|
+
expect(data.colorMatrix).toEqual(matrix);
|
|
54
|
+
expect(data.colorScaleBias).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('resolves to null when the node carries no adjustments', () => {
|
|
58
|
+
const state = createEnabledRenderState();
|
|
59
|
+
const node = createDisplayObject();
|
|
60
|
+
const data = createRenderProxy(state, node as unknown as Renderable);
|
|
61
|
+
resolveColorAdjustments(state, data);
|
|
62
|
+
expect(data.colorScaleBias).toBeNull();
|
|
63
|
+
expect(data.colorMatrix).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('inherits a parent adjustment when the renderer-bearing child has none of its own', () => {
|
|
67
|
+
const state = createEnabledRenderState();
|
|
68
|
+
const parent = createDisplayObject();
|
|
69
|
+
const child = createDisplayObject();
|
|
70
|
+
setNodeColorAdjustments(parent, [createTintAdjustment(0x80ff33ff)]);
|
|
71
|
+
const parentData = createRenderProxy(state, parent as unknown as Renderable);
|
|
72
|
+
const childData = createRenderProxy(state, child as unknown as Renderable);
|
|
73
|
+
resolveColorAdjustments(state, parentData);
|
|
74
|
+
resolveColorAdjustments(state, childData, parentData);
|
|
75
|
+
expect(childData.colorScaleBias).toBe(parentData.colorScaleBias);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('carries a container adjustment to its child through the real render walk', () => {
|
|
79
|
+
const state = createEnabledRenderState();
|
|
80
|
+
const parent = createDisplayObject();
|
|
81
|
+
const child = createDisplayObject();
|
|
82
|
+
addNodeChild(parent, child);
|
|
83
|
+
setNodeColorAdjustments(parent, [createTintAdjustment(0x80ff33ff)]);
|
|
84
|
+
prepareScene2DRender(state, parent);
|
|
85
|
+
expect(getRenderProxy2D(state, child)?.colorScaleBias).toBe(getRenderProxy2D(state, parent)?.colorScaleBias);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('refreshes inherited adjustments through descendants when an ancestor changes', () => {
|
|
89
|
+
const state = createEnabledRenderState();
|
|
90
|
+
const root = createDisplayObject();
|
|
91
|
+
const parent = createDisplayObject();
|
|
92
|
+
const child = createDisplayObject();
|
|
93
|
+
addNodeChild(root, parent);
|
|
94
|
+
addNodeChild(parent, child);
|
|
95
|
+
setNodeColorAdjustments(root, [createTintAdjustment(0x80ffffff)]);
|
|
96
|
+
setNodeColorAdjustments(parent, [createTintAdjustment(0xff8033ff)]);
|
|
97
|
+
|
|
98
|
+
prepareScene2DRender(state, root);
|
|
99
|
+
const first = getRenderProxy2D(state, child)?.colorScaleBias;
|
|
100
|
+
expect(first?.redScale).toBeCloseTo(0.5 * 1);
|
|
101
|
+
expect(first?.greenScale).toBeCloseTo(1 * 0.5);
|
|
102
|
+
|
|
103
|
+
setNodeColorAdjustments(root, [createTintAdjustment(0x40ffffff)]);
|
|
104
|
+
prepareScene2DRender(state, root);
|
|
105
|
+
const second = getRenderProxy2D(state, child)?.colorScaleBias;
|
|
106
|
+
expect(second?.redScale).toBeCloseTo(0.25 * 1);
|
|
107
|
+
expect(second?.greenScale).toBeCloseTo(1 * 0.5);
|
|
108
|
+
|
|
109
|
+
setNodeColorAdjustments(parent, null);
|
|
110
|
+
prepareScene2DRender(state, root);
|
|
111
|
+
expect(getRenderProxy2D(state, child)?.colorScaleBias).toBe(getRenderProxy2D(state, root)?.colorScaleBias);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('concatenates a parent affine adjustment after the child adjustment', () => {
|
|
115
|
+
const state = createEnabledRenderState();
|
|
116
|
+
const parent = createDisplayObject();
|
|
117
|
+
const child = createDisplayObject();
|
|
118
|
+
setNodeColorAdjustments(parent, [
|
|
119
|
+
createColorScaleBiasAdjustment({
|
|
120
|
+
redScale: 0.5,
|
|
121
|
+
redBias: 0.1,
|
|
122
|
+
greenScale: 1,
|
|
123
|
+
greenBias: 0,
|
|
124
|
+
blueScale: 1,
|
|
125
|
+
blueBias: 0,
|
|
126
|
+
alphaScale: 1,
|
|
127
|
+
alphaBias: 0,
|
|
128
|
+
}),
|
|
129
|
+
]);
|
|
130
|
+
setNodeColorAdjustments(child, [
|
|
131
|
+
createColorScaleBiasAdjustment({
|
|
132
|
+
redScale: 0.25,
|
|
133
|
+
redBias: 0.2,
|
|
134
|
+
greenScale: 1,
|
|
135
|
+
greenBias: 0,
|
|
136
|
+
blueScale: 1,
|
|
137
|
+
blueBias: 0,
|
|
138
|
+
alphaScale: 1,
|
|
139
|
+
alphaBias: 0,
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
const parentData = createRenderProxy(state, parent as unknown as Renderable);
|
|
143
|
+
const childData = createRenderProxy(state, child as unknown as Renderable);
|
|
144
|
+
resolveColorAdjustments(state, parentData);
|
|
145
|
+
resolveColorAdjustments(state, childData, parentData);
|
|
146
|
+
expect(childData.colorScaleBias?.redScale).toBeCloseTo(0.125);
|
|
147
|
+
expect(childData.colorScaleBias?.redBias).toBeCloseTo(0.2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('promotes a parent affine and child matrix to one parent-after-child matrix', () => {
|
|
151
|
+
const state = createEnabledRenderState();
|
|
152
|
+
const parent = createDisplayObject();
|
|
153
|
+
const child = createDisplayObject();
|
|
154
|
+
const parentAdjustment = createColorScaleBiasAdjustment({
|
|
155
|
+
redScale: 0.5,
|
|
156
|
+
redBias: 0.1,
|
|
157
|
+
greenScale: 1,
|
|
158
|
+
greenBias: 0,
|
|
159
|
+
blueScale: 1,
|
|
160
|
+
blueBias: 0,
|
|
161
|
+
alphaScale: 1,
|
|
162
|
+
alphaBias: 0,
|
|
163
|
+
});
|
|
164
|
+
const childMatrix = [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
|
|
165
|
+
setNodeColorAdjustments(parent, [parentAdjustment]);
|
|
166
|
+
setNodeColorAdjustments(child, [createColorMatrixAdjustment(childMatrix)]);
|
|
167
|
+
const parentData = createRenderProxy(state, parent as unknown as Renderable);
|
|
168
|
+
const childData = createRenderProxy(state, child as unknown as Renderable);
|
|
169
|
+
resolveColorAdjustments(state, parentData);
|
|
170
|
+
resolveColorAdjustments(state, childData, parentData);
|
|
171
|
+
const source = 0x204060ff;
|
|
172
|
+
const expected = applyColorMatrixToColor(
|
|
173
|
+
parentAdjustment.colorMatrix,
|
|
174
|
+
applyColorMatrixToColor(childMatrix, source),
|
|
175
|
+
);
|
|
176
|
+
expect(applyColorMatrixToColor(childData.colorMatrix!, source)).toBe(expected);
|
|
177
|
+
expect(childData.colorScaleBias).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('promotes a parent matrix and child affine to one parent-after-child matrix', () => {
|
|
181
|
+
const state = createEnabledRenderState();
|
|
182
|
+
const parent = createDisplayObject();
|
|
183
|
+
const child = createDisplayObject();
|
|
184
|
+
const parentMatrix = [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0];
|
|
185
|
+
const childAdjustment = createColorScaleBiasAdjustment({
|
|
186
|
+
redScale: 0.5,
|
|
187
|
+
redBias: 0.1,
|
|
188
|
+
greenScale: 1,
|
|
189
|
+
greenBias: 0,
|
|
190
|
+
blueScale: 1,
|
|
191
|
+
blueBias: 0,
|
|
192
|
+
alphaScale: 1,
|
|
193
|
+
alphaBias: 0,
|
|
194
|
+
});
|
|
195
|
+
setNodeColorAdjustments(parent, [createColorMatrixAdjustment(parentMatrix)]);
|
|
196
|
+
setNodeColorAdjustments(child, [childAdjustment]);
|
|
197
|
+
const parentData = createRenderProxy(state, parent as unknown as Renderable);
|
|
198
|
+
const childData = createRenderProxy(state, child as unknown as Renderable);
|
|
199
|
+
resolveColorAdjustments(state, parentData);
|
|
200
|
+
resolveColorAdjustments(state, childData, parentData);
|
|
201
|
+
const source = 0x204060ff;
|
|
202
|
+
const expected = applyColorMatrixToColor(
|
|
203
|
+
parentMatrix,
|
|
204
|
+
applyColorMatrixToColor(childAdjustment.colorMatrix, source),
|
|
205
|
+
);
|
|
206
|
+
expect(applyColorMatrixToColor(childData.colorMatrix!, source)).toBe(expected);
|
|
207
|
+
expect(childData.colorScaleBias).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('reads the cache the accessor fused once (the walk never re-fuses)', () => {
|
|
211
|
+
const state = createEnabledRenderState();
|
|
212
|
+
const node = createDisplayObject();
|
|
213
|
+
setNodeColorAdjustments(node, [createTintAdjustment(0x3fffffff)]);
|
|
214
|
+
// The set-accessor fused the stack once; the runtime already holds the cached resolved value.
|
|
215
|
+
const cached = getNode2DRuntime(node).resolvedColorScaleBias;
|
|
216
|
+
expect(cached).not.toBeNull();
|
|
217
|
+
const data = createRenderProxy(state, node as unknown as Renderable);
|
|
218
|
+
resolveColorAdjustments(state, data);
|
|
219
|
+
expect(data.colorScaleBias).toBe(cached);
|
|
220
|
+
resolveColorAdjustments(state, data);
|
|
221
|
+
expect(data.colorScaleBias).toBe(cached);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function createEnabledRenderState(): RenderState {
|
|
226
|
+
const state = createRenderState();
|
|
227
|
+
enableColorAdjustments(state);
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveColorAdjustments(state: RenderState, data: RenderProxy, parentData?: RenderProxy): void {
|
|
232
|
+
getRenderStateRuntime(state).colorAdjustmentResolver!(state, data, parentData);
|
|
233
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCamera3D,
|
|
3
|
+
createPerspectiveProjection,
|
|
4
|
+
setCamera3DViewMatrix4FromLookAt,
|
|
5
|
+
} from '@flighthq/camera/contract';
|
|
6
|
+
import { createAabb } from '@flighthq/geometry/contract';
|
|
7
|
+
import { createAmbientLight, createDirectionalLight } from '@flighthq/lighting/contract';
|
|
8
|
+
import { setLogSink } from '@flighthq/log/contract';
|
|
9
|
+
import { computeMeshGeometryBounds, createBoxMeshGeometry } from '@flighthq/mesh/contract';
|
|
10
|
+
import { addNodeChild } from '@flighthq/node/contract';
|
|
11
|
+
import { createMesh, createNode3D, Node3DKind } from '@flighthq/scene3d/contract';
|
|
12
|
+
import type { Camera3D, LogEntry, Mesh, MeshGeometry, Skin } from '@flighthq/types/contract';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { disableSceneRenderGuards, enableSceneRenderGuards } from './enableSceneRenderGuards';
|
|
16
|
+
import { createRenderState } from './renderState';
|
|
17
|
+
import { prepareScene3DRender } from './sceneRender';
|
|
18
|
+
|
|
19
|
+
let entries: LogEntry[];
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
entries = [];
|
|
23
|
+
setLogSink((entry) => entries.push(entry));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
disableSceneRenderGuards();
|
|
28
|
+
setLogSink(null);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function messages(): string {
|
|
32
|
+
return entries.map((e) => String((e.data as { message?: unknown } | undefined)?.message ?? '')).join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function boundedBox(): MeshGeometry {
|
|
36
|
+
const geometry = createBoxMeshGeometry(2, 2, 2);
|
|
37
|
+
const bounds = createAabb();
|
|
38
|
+
computeMeshGeometryBounds(bounds, geometry);
|
|
39
|
+
geometry.bounds = bounds;
|
|
40
|
+
return geometry;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function frontCamera(): Camera3D {
|
|
44
|
+
const camera = createCamera3D({
|
|
45
|
+
far: 100,
|
|
46
|
+
near: 0.1,
|
|
47
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 3 }),
|
|
48
|
+
});
|
|
49
|
+
setCamera3DViewMatrix4FromLookAt(camera, { x: 0, y: 0, z: 10 }, { x: 0, y: 0, z: 0 }, { x: 0, y: 1, z: 0 });
|
|
50
|
+
return camera;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function prepareWith(mesh: Mesh): void {
|
|
54
|
+
const state = createRenderState();
|
|
55
|
+
const scene = createNode3D(Node3DKind);
|
|
56
|
+
addNodeChild(scene, mesh);
|
|
57
|
+
prepareScene3DRender(state, scene, frontCamera(), {
|
|
58
|
+
ambient: createAmbientLight(),
|
|
59
|
+
directional: createDirectionalLight(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('disableSceneRenderGuards', () => {
|
|
64
|
+
it('stops the guard reporting later frames', () => {
|
|
65
|
+
enableSceneRenderGuards();
|
|
66
|
+
disableSceneRenderGuards();
|
|
67
|
+
const mesh = createMesh(boundedBox(), [null]);
|
|
68
|
+
mesh.skin = {} as Skin;
|
|
69
|
+
prepareWith(mesh);
|
|
70
|
+
expect(messages()).toBe('');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('enableSceneRenderGuards', () => {
|
|
75
|
+
// A rigid mesh legitimately has no posed bounds — it never deforms — so warning about it would make
|
|
76
|
+
// the guard noise on the common path and train people to ignore it.
|
|
77
|
+
it('says nothing for a rigid mesh with no posed bounds', () => {
|
|
78
|
+
enableSceneRenderGuards();
|
|
79
|
+
prepareWith(createMesh(boundedBox(), [null]));
|
|
80
|
+
expect(messages()).toBe('');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// The failure this catches is invisible where it happens: culling falls back to BIND POSE bounds and
|
|
84
|
+
// a swung limb is removed from the frame, which reads as a culling or camera bug rather than a
|
|
85
|
+
// missing call in the frame loop.
|
|
86
|
+
it('warns when a skinned mesh reaches culling with no posed bounds', () => {
|
|
87
|
+
enableSceneRenderGuards();
|
|
88
|
+
const mesh = createMesh(boundedBox(), [null]);
|
|
89
|
+
mesh.skin = {} as Skin;
|
|
90
|
+
|
|
91
|
+
prepareWith(mesh);
|
|
92
|
+
|
|
93
|
+
expect(messages()).toContain('BIND POSE');
|
|
94
|
+
expect(messages()).toContain('prepareScene3DSkinning');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Renderer, Scene2DKindUsage, SceneCoverageEntry } from '@flighthq/types/contract';
|
|
2
|
+
import { RenderRegistry, SceneCoverage } from '@flighthq/types/contract';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { explainScene2DCoverage, hasScene2DCoverage } from './explainScene2DCoverage';
|
|
6
|
+
import { registerRenderer } from './renderer';
|
|
7
|
+
import { createRenderState } from './renderState';
|
|
8
|
+
import { getRenderStateRuntime } from './renderState';
|
|
9
|
+
|
|
10
|
+
const renderer: Renderer = { createData: () => null, submit: () => {} } as unknown as Renderer;
|
|
11
|
+
|
|
12
|
+
function usage(overrides: Partial<Scene2DKindUsage> = {}): Scene2DKindUsage {
|
|
13
|
+
return { blendModes: [], materialKinds: [], nodeKinds: [], shapeCommandKeys: [], ...overrides };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function entries(state: ReturnType<typeof createRenderState>, u: Scene2DKindUsage): SceneCoverageEntry[] {
|
|
17
|
+
const out: SceneCoverageEntry[] = [];
|
|
18
|
+
explainScene2DCoverage(out, state, u);
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The registry is a plain Map on the base runtime; registering a real command means reaching through
|
|
23
|
+
// scene2d-canvas, which render cannot depend on, so the test installs the binding directly.
|
|
24
|
+
function wireShapeCommand(state: ReturnType<typeof createRenderState>, key: string): void {
|
|
25
|
+
const runtime = getRenderStateRuntime(state);
|
|
26
|
+
(runtime.canvasShapeCommandRegistry ??= new Map()).set(key, { key, draw: () => {} } as never);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('explainScene2DCoverage', () => {
|
|
30
|
+
it('reports an unregistered node kind as Missing, since the node and its subtree never draw', () => {
|
|
31
|
+
expect(entries(createRenderState(), usage({ nodeKinds: ['Shape'] }))).toEqual([
|
|
32
|
+
{ coverage: SceneCoverage.Missing, kind: 'Shape', registry: RenderRegistry.NodeRenderer },
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('reports a registered node kind as Satisfied rather than omitting it', () => {
|
|
37
|
+
const state = createRenderState();
|
|
38
|
+
registerRenderer(state, 'Shape', renderer);
|
|
39
|
+
expect(entries(state, usage({ nodeKinds: ['Shape'] }))).toEqual([
|
|
40
|
+
{ coverage: SceneCoverage.Satisfied, kind: 'Shape', registry: RenderRegistry.NodeRenderer },
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('reports an unhandled shape command key against the state that would replay it', () => {
|
|
45
|
+
expect(entries(createRenderState(), usage({ shapeCommandKeys: ['beginFill'] }))).toContainEqual({
|
|
46
|
+
coverage: SceneCoverage.Missing,
|
|
47
|
+
kind: 'beginFill',
|
|
48
|
+
registry: RenderRegistry.ShapeCommandHandler,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reports a wired shape command key as Satisfied', () => {
|
|
53
|
+
const state = createRenderState();
|
|
54
|
+
wireShapeCommand(state, 'beginFill');
|
|
55
|
+
expect(entries(state, usage({ shapeCommandKeys: ['beginFill'] }))).toContainEqual({
|
|
56
|
+
coverage: SceneCoverage.Satisfied,
|
|
57
|
+
kind: 'beginFill',
|
|
58
|
+
registry: RenderRegistry.ShapeCommandHandler,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('answers both registries in one manifest', () => {
|
|
63
|
+
const state = createRenderState();
|
|
64
|
+
registerRenderer(state, 'Shape', renderer);
|
|
65
|
+
const found = entries(state, usage({ nodeKinds: ['Shape', 'Sprite'], shapeCommandKeys: ['beginFill'] }));
|
|
66
|
+
expect(found).toHaveLength(3);
|
|
67
|
+
expect(found.filter((e) => e.coverage === SceneCoverage.Missing).map((e) => e.kind)).toEqual([
|
|
68
|
+
'Sprite',
|
|
69
|
+
'beginFill',
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('clears out, so a repeated call does not accumulate', () => {
|
|
74
|
+
const state = createRenderState();
|
|
75
|
+
const u = usage({ nodeKinds: ['Shape'] });
|
|
76
|
+
const out: SceneCoverageEntry[] = [];
|
|
77
|
+
explainScene2DCoverage(out, state, u);
|
|
78
|
+
const first = out.length;
|
|
79
|
+
explainScene2DCoverage(out, state, u);
|
|
80
|
+
expect(out).toHaveLength(first);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('hasScene2DCoverage', () => {
|
|
85
|
+
it('is false while any node kind is unserved', () => {
|
|
86
|
+
expect(hasScene2DCoverage(createRenderState(), usage({ nodeKinds: ['Shape'] }))).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('stays true when the manifest is all Satisfied entries, which are not shortfalls', () => {
|
|
90
|
+
const state = createRenderState();
|
|
91
|
+
registerRenderer(state, 'Shape', renderer);
|
|
92
|
+
wireShapeCommand(state, 'beginFill');
|
|
93
|
+
const u = usage({ nodeKinds: ['Shape'], shapeCommandKeys: ['beginFill'] });
|
|
94
|
+
const out: SceneCoverageEntry[] = [];
|
|
95
|
+
explainScene2DCoverage(out, state, u);
|
|
96
|
+
expect(out.every((e) => e.coverage === SceneCoverage.Satisfied)).toBe(true);
|
|
97
|
+
expect(hasScene2DCoverage(state, u)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('agrees with the explain tier, so the two can never disagree', () => {
|
|
101
|
+
const state = createRenderState();
|
|
102
|
+
registerRenderer(state, 'Shape', renderer);
|
|
103
|
+
const u = usage({ nodeKinds: ['Shape', 'Sprite'] });
|
|
104
|
+
const out: SceneCoverageEntry[] = [];
|
|
105
|
+
explainScene2DCoverage(out, state, u);
|
|
106
|
+
const gaps = out.filter((e) => e.coverage !== SceneCoverage.Satisfied);
|
|
107
|
+
expect(hasScene2DCoverage(state, u)).toBe(gaps.length === 0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('is true for a scene that names no kinds at all', () => {
|
|
111
|
+
expect(hasScene2DCoverage(createRenderState(), usage())).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|