@flighthq/render 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/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/renderAppearance.d.ts +3 -0
- package/dist/renderAppearance.d.ts.map +1 -0
- package/dist/renderAppearance.js +35 -0
- package/dist/renderAppearance.js.map +1 -0
- package/dist/renderCache.d.ts +36 -0
- package/dist/renderCache.d.ts.map +1 -0
- package/dist/renderCache.js +83 -0
- package/dist/renderCache.js.map +1 -0
- package/dist/renderColor.d.ts +3 -0
- package/dist/renderColor.d.ts.map +1 -0
- package/dist/renderColor.js +17 -0
- package/dist/renderColor.js.map +1 -0
- package/dist/renderMaterial.d.ts +3 -0
- package/dist/renderMaterial.d.ts.map +1 -0
- package/dist/renderMaterial.js +9 -0
- package/dist/renderMaterial.js.map +1 -0
- package/dist/renderProxy.d.ts +19 -0
- package/dist/renderProxy.d.ts.map +1 -0
- package/dist/renderProxy.js +203 -0
- package/dist/renderProxy.js.map +1 -0
- package/dist/renderProxyAdapter.d.ts +7 -0
- package/dist/renderProxyAdapter.d.ts.map +1 -0
- package/dist/renderProxyAdapter.js +32 -0
- package/dist/renderProxyAdapter.js.map +1 -0
- package/dist/renderQueue.d.ts +9 -0
- package/dist/renderQueue.d.ts.map +1 -0
- package/dist/renderQueue.js +97 -0
- package/dist/renderQueue.js.map +1 -0
- package/dist/renderState.d.ts +5 -0
- package/dist/renderState.d.ts.map +1 -0
- package/dist/renderState.js +42 -0
- package/dist/renderState.js.map +1 -0
- package/dist/renderTarget.d.ts +21 -0
- package/dist/renderTarget.d.ts.map +1 -0
- package/dist/renderTarget.js +39 -0
- package/dist/renderTarget.js.map +1 -0
- package/dist/renderTransform2d.d.ts +3 -0
- package/dist/renderTransform2d.d.ts.map +1 -0
- package/dist/renderTransform2d.js +26 -0
- package/dist/renderTransform2d.js.map +1 -0
- package/dist/renderViewport.d.ts +6 -0
- package/dist/renderViewport.d.ts.map +1 -0
- package/dist/renderViewport.js +57 -0
- package/dist/renderViewport.js.map +1 -0
- package/dist/renderer.d.ts +6 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +24 -0
- package/dist/renderer.js.map +1 -0
- package/dist/sceneRender.d.ts +4 -0
- package/dist/sceneRender.d.ts.map +1 -0
- package/dist/sceneRender.js +175 -0
- package/dist/sceneRender.js.map +1 -0
- package/package.json +48 -0
- package/src/renderAppearance.test.ts +109 -0
- package/src/renderCache.test.ts +151 -0
- package/src/renderColor.test.ts +10 -0
- package/src/renderMaterial.test.ts +30 -0
- package/src/renderProxy.test.ts +531 -0
- package/src/renderProxyAdapter.test.ts +101 -0
- package/src/renderQueue.test.ts +210 -0
- package/src/renderState.test.ts +93 -0
- package/src/renderTarget.test.ts +84 -0
- package/src/renderTransform2d.test.ts +59 -0
- package/src/renderViewport.test.ts +187 -0
- package/src/renderer.test.ts +128 -0
- package/src/sceneRender.test.ts +207 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { addNodeChild } from '@flighthq/node';
|
|
3
|
+
import { createSprite } from '@flighthq/sprite';
|
|
4
|
+
import type { Renderer, RenderProxy, RenderQueueEntry } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { registerRenderer } from './renderer';
|
|
7
|
+
import { prepareDisplayObjectRender } from './renderProxy';
|
|
8
|
+
import {
|
|
9
|
+
buildRenderQueue,
|
|
10
|
+
clearRenderQueue,
|
|
11
|
+
compareRenderQueueEntries,
|
|
12
|
+
createRenderQueue,
|
|
13
|
+
packRenderSortKey,
|
|
14
|
+
pushRenderQueueEntry,
|
|
15
|
+
sortRenderQueue,
|
|
16
|
+
} from './renderQueue';
|
|
17
|
+
import { createRenderState } from './renderState';
|
|
18
|
+
|
|
19
|
+
function makeRenderer(): Renderer {
|
|
20
|
+
return { createData: () => null, submit: vi.fn() };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('buildRenderQueue', () => {
|
|
24
|
+
it('produces zero entries for a scene with no renderers', () => {
|
|
25
|
+
const state = createRenderState();
|
|
26
|
+
const root = createDisplayObject();
|
|
27
|
+
prepareDisplayObjectRender(state, root);
|
|
28
|
+
const queue = createRenderQueue();
|
|
29
|
+
buildRenderQueue(state, root, queue);
|
|
30
|
+
expect(queue.entryCount).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
it('produces one entry per visible node with a renderer', () => {
|
|
33
|
+
const state = createRenderState();
|
|
34
|
+
const root = createDisplayObject();
|
|
35
|
+
const child = createDisplayObject();
|
|
36
|
+
addNodeChild(root, child);
|
|
37
|
+
registerRenderer(state, root.kind, makeRenderer());
|
|
38
|
+
prepareDisplayObjectRender(state, root);
|
|
39
|
+
const queue = createRenderQueue();
|
|
40
|
+
buildRenderQueue(state, root, queue);
|
|
41
|
+
expect(queue.entryCount).toBe(2);
|
|
42
|
+
});
|
|
43
|
+
it('skips invisible proxies', () => {
|
|
44
|
+
const state = createRenderState();
|
|
45
|
+
const root = createDisplayObject();
|
|
46
|
+
root.visible = false;
|
|
47
|
+
registerRenderer(state, root.kind, makeRenderer());
|
|
48
|
+
prepareDisplayObjectRender(state, root);
|
|
49
|
+
const queue = createRenderQueue();
|
|
50
|
+
buildRenderQueue(state, root, queue);
|
|
51
|
+
expect(queue.entryCount).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
it('clears the queue before filling it', () => {
|
|
54
|
+
const state = createRenderState();
|
|
55
|
+
const root = createDisplayObject();
|
|
56
|
+
registerRenderer(state, root.kind, makeRenderer());
|
|
57
|
+
prepareDisplayObjectRender(state, root);
|
|
58
|
+
const queue = createRenderQueue();
|
|
59
|
+
buildRenderQueue(state, root, queue);
|
|
60
|
+
const firstCount = queue.entryCount;
|
|
61
|
+
buildRenderQueue(state, root, queue);
|
|
62
|
+
expect(queue.entryCount).toBe(firstCount);
|
|
63
|
+
});
|
|
64
|
+
it('assigns increasing scene-order sort keys', () => {
|
|
65
|
+
const state = createRenderState();
|
|
66
|
+
const root = createDisplayObject();
|
|
67
|
+
const child1 = createDisplayObject();
|
|
68
|
+
const child2 = createSprite();
|
|
69
|
+
addNodeChild(root, child1);
|
|
70
|
+
addNodeChild(root, child2);
|
|
71
|
+
registerRenderer(state, root.kind, makeRenderer());
|
|
72
|
+
registerRenderer(state, child1.kind, makeRenderer());
|
|
73
|
+
registerRenderer(state, child2.kind, makeRenderer());
|
|
74
|
+
prepareDisplayObjectRender(state, root);
|
|
75
|
+
const queue = createRenderQueue();
|
|
76
|
+
buildRenderQueue(state, root, queue);
|
|
77
|
+
expect(queue.entries[0].sortKey).toBeLessThan(queue.entries[1].sortKey);
|
|
78
|
+
expect(queue.entries[1].sortKey).toBeLessThan(queue.entries[2].sortKey);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('clearRenderQueue', () => {
|
|
83
|
+
it('resets entryCount to zero', () => {
|
|
84
|
+
const queue = createRenderQueue();
|
|
85
|
+
const proxy = { alpha: 1 } as unknown as RenderProxy;
|
|
86
|
+
pushRenderQueueEntry(queue, proxy, 0);
|
|
87
|
+
clearRenderQueue(queue);
|
|
88
|
+
expect(queue.entryCount).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
it('preserves entries array capacity', () => {
|
|
91
|
+
const queue = createRenderQueue();
|
|
92
|
+
const proxy = { alpha: 1 } as unknown as RenderProxy;
|
|
93
|
+
pushRenderQueueEntry(queue, proxy, 0);
|
|
94
|
+
const cap = queue.entries.length;
|
|
95
|
+
clearRenderQueue(queue);
|
|
96
|
+
expect(queue.entries.length).toBe(cap);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('compareRenderQueueEntries', () => {
|
|
101
|
+
it('returns negative when first sort key is smaller', () => {
|
|
102
|
+
const a = { proxy: {}, sortKey: 1 } as unknown as RenderQueueEntry;
|
|
103
|
+
const b = { proxy: {}, sortKey: 5 } as unknown as RenderQueueEntry;
|
|
104
|
+
expect(compareRenderQueueEntries(a, b)).toBeLessThan(0);
|
|
105
|
+
});
|
|
106
|
+
it('returns positive when first sort key is larger', () => {
|
|
107
|
+
const a = { proxy: {}, sortKey: 5 } as unknown as RenderQueueEntry;
|
|
108
|
+
const b = { proxy: {}, sortKey: 1 } as unknown as RenderQueueEntry;
|
|
109
|
+
expect(compareRenderQueueEntries(a, b)).toBeGreaterThan(0);
|
|
110
|
+
});
|
|
111
|
+
it('returns 0 when sort keys are equal', () => {
|
|
112
|
+
const a = { proxy: {}, sortKey: 3 } as unknown as RenderQueueEntry;
|
|
113
|
+
const b = { proxy: {}, sortKey: 3 } as unknown as RenderQueueEntry;
|
|
114
|
+
expect(compareRenderQueueEntries(a, b)).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('createRenderQueue', () => {
|
|
119
|
+
it('starts with entryCount of 0', () => {
|
|
120
|
+
expect(createRenderQueue().entryCount).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
it('starts with an empty entries array', () => {
|
|
123
|
+
expect(createRenderQueue().entries.length).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('packRenderSortKey', () => {
|
|
128
|
+
it('opaque geometry sorts before transparent in the same layer', () => {
|
|
129
|
+
const opaque = packRenderSortKey(0, 0.5, false);
|
|
130
|
+
const transparent = packRenderSortKey(0, 0.5, true);
|
|
131
|
+
expect(opaque).toBeLessThan(transparent);
|
|
132
|
+
});
|
|
133
|
+
it('higher layer always sorts after lower layer regardless of opacity', () => {
|
|
134
|
+
const layer0Transparent = packRenderSortKey(0, 1.0, true);
|
|
135
|
+
const layer1Opaque = packRenderSortKey(1, 0.0, false);
|
|
136
|
+
expect(layer1Opaque).toBeGreaterThan(layer0Transparent);
|
|
137
|
+
});
|
|
138
|
+
it('depth increases sort key within the same layer and opacity', () => {
|
|
139
|
+
const near = packRenderSortKey(0, 0.1, false);
|
|
140
|
+
const far = packRenderSortKey(0, 0.9, false);
|
|
141
|
+
expect(far).toBeGreaterThan(near);
|
|
142
|
+
});
|
|
143
|
+
it('clamps layer and depth to valid range', () => {
|
|
144
|
+
expect(() => packRenderSortKey(-1, -0.5, false)).not.toThrow();
|
|
145
|
+
expect(() => packRenderSortKey(99999, 2.0, true)).not.toThrow();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('pushRenderQueueEntry', () => {
|
|
150
|
+
it('increments entryCount', () => {
|
|
151
|
+
const queue = createRenderQueue();
|
|
152
|
+
const proxy = { alpha: 1 } as unknown as RenderProxy;
|
|
153
|
+
pushRenderQueueEntry(queue, proxy, 0);
|
|
154
|
+
expect(queue.entryCount).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
it('stores the proxy and sort key', () => {
|
|
157
|
+
const queue = createRenderQueue();
|
|
158
|
+
const proxy = { alpha: 1 } as unknown as RenderProxy;
|
|
159
|
+
pushRenderQueueEntry(queue, proxy, 42);
|
|
160
|
+
expect(queue.entries[0].proxy).toBe(proxy);
|
|
161
|
+
expect(queue.entries[0].sortKey).toBe(42);
|
|
162
|
+
});
|
|
163
|
+
it('reuses entries array slots within capacity', () => {
|
|
164
|
+
const queue = createRenderQueue();
|
|
165
|
+
const p1 = { alpha: 1 } as unknown as RenderProxy;
|
|
166
|
+
const p2 = { alpha: 2 } as unknown as RenderProxy;
|
|
167
|
+
pushRenderQueueEntry(queue, p1, 0);
|
|
168
|
+
clearRenderQueue(queue);
|
|
169
|
+
pushRenderQueueEntry(queue, p2, 1);
|
|
170
|
+
expect(queue.entries.length).toBe(1);
|
|
171
|
+
expect(queue.entries[0].proxy).toBe(p2);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('sortRenderQueue', () => {
|
|
176
|
+
it('sorts entries in ascending sort-key order by default', () => {
|
|
177
|
+
const queue = createRenderQueue();
|
|
178
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 3);
|
|
179
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 1);
|
|
180
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 2);
|
|
181
|
+
sortRenderQueue(queue);
|
|
182
|
+
expect(queue.entries[0].sortKey).toBe(1);
|
|
183
|
+
expect(queue.entries[1].sortKey).toBe(2);
|
|
184
|
+
expect(queue.entries[2].sortKey).toBe(3);
|
|
185
|
+
});
|
|
186
|
+
it('accepts a custom comparator', () => {
|
|
187
|
+
const queue = createRenderQueue();
|
|
188
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 1);
|
|
189
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 3);
|
|
190
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 2);
|
|
191
|
+
sortRenderQueue(queue, (a, b) => b.sortKey - a.sortKey); // descending
|
|
192
|
+
expect(queue.entries[0].sortKey).toBe(3);
|
|
193
|
+
expect(queue.entries[1].sortKey).toBe(2);
|
|
194
|
+
expect(queue.entries[2].sortKey).toBe(1);
|
|
195
|
+
});
|
|
196
|
+
it('only sorts the valid entry window [0..entryCount)', () => {
|
|
197
|
+
const queue = createRenderQueue();
|
|
198
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 3);
|
|
199
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 1);
|
|
200
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 2);
|
|
201
|
+
// Clear and add fewer entries (leaving stale data beyond entryCount).
|
|
202
|
+
clearRenderQueue(queue);
|
|
203
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 5);
|
|
204
|
+
pushRenderQueueEntry(queue, {} as unknown as RenderProxy, 4);
|
|
205
|
+
sortRenderQueue(queue);
|
|
206
|
+
expect(queue.entryCount).toBe(2);
|
|
207
|
+
expect(queue.entries[0].sortKey).toBe(4);
|
|
208
|
+
expect(queue.entries[1].sortKey).toBe(5);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import type { RenderState } from '@flighthq/types';
|
|
3
|
+
import { BlendMode, EntityRuntimeKey } from '@flighthq/types';
|
|
4
|
+
|
|
5
|
+
import { createRenderState, createRenderStateRuntime, getRenderStateRuntime } from './renderState';
|
|
6
|
+
|
|
7
|
+
describe('createRenderState', () => {
|
|
8
|
+
let state: RenderState;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
state = createRenderState();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('initializes default values', () => {
|
|
15
|
+
expect(state.allowSmoothing).toStrictEqual(true);
|
|
16
|
+
expect(state.backgroundColor).toStrictEqual(0);
|
|
17
|
+
expect(state.backgroundColorRgba).toStrictEqual([]);
|
|
18
|
+
expect(state.backgroundColorString).toStrictEqual('');
|
|
19
|
+
expect(state.currentClipDepth).toStrictEqual(0);
|
|
20
|
+
expect(state.pixelRatio).toStrictEqual(1);
|
|
21
|
+
expect(state.renderAlpha).toStrictEqual(1);
|
|
22
|
+
expect(state.renderBlendMode).toStrictEqual(BlendMode.Normal);
|
|
23
|
+
expect(state.displayObjectClipHooks).toStrictEqual(null);
|
|
24
|
+
expect(state.renderTransform2D).toStrictEqual(null);
|
|
25
|
+
expect(state.roundPixels).toStrictEqual(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('attaches a render state runtime with the machinery fields', () => {
|
|
29
|
+
const runtime = getRenderStateRuntime(state);
|
|
30
|
+
expect(runtime.currentFrameId).toStrictEqual(0);
|
|
31
|
+
expect(runtime.renderProxyMap).toStrictEqual(new WeakMap());
|
|
32
|
+
expect(runtime.renderProxyAdapterMap).toStrictEqual(new WeakMap());
|
|
33
|
+
expect(runtime.rendererMap).toStrictEqual(new Map());
|
|
34
|
+
expect(runtime.rendererMapId).toStrictEqual(0);
|
|
35
|
+
expect(runtime.tempStack).toStrictEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('allows pre-defined values', () => {
|
|
39
|
+
const base = {
|
|
40
|
+
allowSmoothing: false,
|
|
41
|
+
backgroundColor: 0xff,
|
|
42
|
+
backgroundColorRgba: [1, 0, 0, 0],
|
|
43
|
+
backgroundColorString: '#FF000000',
|
|
44
|
+
pixelRatio: 5,
|
|
45
|
+
renderAlpha: 0.5,
|
|
46
|
+
renderBlendMode: BlendMode.Multiply,
|
|
47
|
+
renderTransform2D: createMatrix(),
|
|
48
|
+
roundPixels: true,
|
|
49
|
+
};
|
|
50
|
+
const obj = createRenderState(base);
|
|
51
|
+
expect(obj.allowSmoothing).toStrictEqual(base.allowSmoothing);
|
|
52
|
+
expect(obj.backgroundColor).toStrictEqual(base.backgroundColor);
|
|
53
|
+
expect(obj.backgroundColorRgba).toStrictEqual(base.backgroundColorRgba);
|
|
54
|
+
expect(obj.backgroundColorString).toStrictEqual(base.backgroundColorString);
|
|
55
|
+
expect(obj.pixelRatio).toStrictEqual(base.pixelRatio);
|
|
56
|
+
expect(obj.renderAlpha).toStrictEqual(base.renderAlpha);
|
|
57
|
+
expect(obj.renderBlendMode).toStrictEqual(base.renderBlendMode);
|
|
58
|
+
expect(obj.renderTransform2D).toStrictEqual(base.renderTransform2D);
|
|
59
|
+
expect(obj.roundPixels).toStrictEqual(base.roundPixels);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns a new object for better hidden-class performance', () => {
|
|
63
|
+
const base = {};
|
|
64
|
+
const obj = createRenderState(base);
|
|
65
|
+
expect(obj).not.toStrictEqual(base);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('createRenderStateRuntime', () => {
|
|
70
|
+
it('initializes the machinery fields', () => {
|
|
71
|
+
const runtime = createRenderStateRuntime();
|
|
72
|
+
expect(runtime.currentFrameId).toStrictEqual(0);
|
|
73
|
+
expect(runtime.renderProxyMap).toStrictEqual(new WeakMap());
|
|
74
|
+
expect(runtime.renderProxyAdapterMap).toStrictEqual(new WeakMap());
|
|
75
|
+
expect(runtime.rendererMap).toStrictEqual(new Map());
|
|
76
|
+
expect(runtime.rendererMapId).toStrictEqual(0);
|
|
77
|
+
expect(runtime.tempStack).toStrictEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns a distinct runtime each call', () => {
|
|
81
|
+
const a = createRenderStateRuntime();
|
|
82
|
+
const b = createRenderStateRuntime();
|
|
83
|
+
expect(a).not.toBe(b);
|
|
84
|
+
expect(a.rendererMap).not.toBe(b.rendererMap);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('getRenderStateRuntime', () => {
|
|
89
|
+
it('returns the runtime attached under EntityRuntimeKey', () => {
|
|
90
|
+
const state = createRenderState();
|
|
91
|
+
expect(getRenderStateRuntime(state)).toBe(state[EntityRuntimeKey]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix, createRectangle } from '@flighthq/geometry';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
computeDisplayObjectRenderTargetTransform,
|
|
6
|
+
computeRenderCacheTransform,
|
|
7
|
+
computeRenderTargetSize,
|
|
8
|
+
} from './renderTarget';
|
|
9
|
+
|
|
10
|
+
describe('computeDisplayObjectRenderTargetTransform', () => {
|
|
11
|
+
it('writes an identity-based transform for an unrotated object at origin', () => {
|
|
12
|
+
const obj = createDisplayObject();
|
|
13
|
+
const bounds = createRectangle(0, 0, 100, 80);
|
|
14
|
+
const out = createMatrix();
|
|
15
|
+
computeDisplayObjectRenderTargetTransform(out, obj, bounds);
|
|
16
|
+
expect(typeof out.a).toBe('number');
|
|
17
|
+
expect(typeof out.tx).toBe('number');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('offsets by contentX and contentY', () => {
|
|
21
|
+
const obj = createDisplayObject();
|
|
22
|
+
const bounds = createRectangle(10, 20, 100, 80);
|
|
23
|
+
const out1 = createMatrix();
|
|
24
|
+
const out2 = createMatrix();
|
|
25
|
+
computeDisplayObjectRenderTargetTransform(out1, obj, bounds, 0, 0);
|
|
26
|
+
computeDisplayObjectRenderTargetTransform(out2, obj, bounds, 5, 10);
|
|
27
|
+
expect(out2.tx).not.toBe(out1.tx);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does not throw for a non-identity local transform', () => {
|
|
31
|
+
const obj = createDisplayObject();
|
|
32
|
+
obj.x = 50;
|
|
33
|
+
obj.y = 30;
|
|
34
|
+
const bounds = createRectangle(50, 30, 100, 80);
|
|
35
|
+
const out = createMatrix();
|
|
36
|
+
expect(() => computeDisplayObjectRenderTargetTransform(out, obj, bounds)).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('computeRenderCacheTransform', () => {
|
|
41
|
+
it('produces a pure translation from bounds origin', () => {
|
|
42
|
+
const out = createMatrix();
|
|
43
|
+
computeRenderCacheTransform(out, { x: 10, y: 20, width: 100, height: 80 });
|
|
44
|
+
expect(out.a).toBe(1);
|
|
45
|
+
expect(out.b).toBe(0);
|
|
46
|
+
expect(out.c).toBe(0);
|
|
47
|
+
expect(out.d).toBe(1);
|
|
48
|
+
expect(out.tx).toBe(10);
|
|
49
|
+
expect(out.ty).toBe(20);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('subtracts contentX and contentY from the translation', () => {
|
|
53
|
+
const out = createMatrix();
|
|
54
|
+
computeRenderCacheTransform(out, { x: 10, y: 20, width: 100, height: 80 }, 3, 7);
|
|
55
|
+
expect(out.tx).toBe(7);
|
|
56
|
+
expect(out.ty).toBe(13);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('computeRenderTargetSize', () => {
|
|
61
|
+
it('returns width and height from bounds', () => {
|
|
62
|
+
const result = computeRenderTargetSize({ x: 0, y: 0, width: 100.4, height: 80.6 });
|
|
63
|
+
expect(result.width).toBe(101);
|
|
64
|
+
expect(result.height).toBe(81);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('adds padding on both sides', () => {
|
|
68
|
+
const result = computeRenderTargetSize({ x: 0, y: 0, width: 100, height: 80 }, 4);
|
|
69
|
+
expect(result.width).toBe(108);
|
|
70
|
+
expect(result.height).toBe(88);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('respects minWidth and minHeight', () => {
|
|
74
|
+
const result = computeRenderTargetSize({ x: 0, y: 0, width: 0, height: 0 }, 0, 32, 16);
|
|
75
|
+
expect(result.width).toBe(32);
|
|
76
|
+
expect(result.height).toBe(16);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('uses defaults of minWidth=1 minHeight=1', () => {
|
|
80
|
+
const result = computeRenderTargetSize({ x: 0, y: 0, width: 0, height: 0 });
|
|
81
|
+
expect(result.width).toBe(1);
|
|
82
|
+
expect(result.height).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
|
|
3
|
+
import { createRenderProxy2D } from './renderProxy';
|
|
4
|
+
import { createRenderState, getRenderStateRuntime } from './renderState';
|
|
5
|
+
import { updateRenderProxy2DTransform } from './renderTransform2d';
|
|
6
|
+
|
|
7
|
+
describe('updateRenderProxy2DTransform', () => {
|
|
8
|
+
it('returns true and updates transform on first call', () => {
|
|
9
|
+
const state = createRenderState();
|
|
10
|
+
const obj = createDisplayObject();
|
|
11
|
+
const data = createRenderProxy2D(state, obj);
|
|
12
|
+
expect(updateRenderProxy2DTransform(state, data)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns false when nothing changed', () => {
|
|
16
|
+
const state = createRenderState();
|
|
17
|
+
const obj = createDisplayObject();
|
|
18
|
+
const data = createRenderProxy2D(state, obj);
|
|
19
|
+
updateRenderProxy2DTransform(state, data);
|
|
20
|
+
expect(updateRenderProxy2DTransform(state, data)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns true when parent was updated this frame', () => {
|
|
24
|
+
const state = createRenderState();
|
|
25
|
+
getRenderStateRuntime(state).currentFrameId = 2;
|
|
26
|
+
const parentObj = createDisplayObject();
|
|
27
|
+
const parentData = createRenderProxy2D(state, parentObj);
|
|
28
|
+
parentData.transformFrameId = getRenderStateRuntime(state).currentFrameId;
|
|
29
|
+
|
|
30
|
+
const obj = createDisplayObject();
|
|
31
|
+
const data = createRenderProxy2D(state, obj);
|
|
32
|
+
updateRenderProxy2DTransform(state, data);
|
|
33
|
+
expect(updateRenderProxy2DTransform(state, data, parentData)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('sets transformFrameId to currentFrameId on update', () => {
|
|
37
|
+
const state = createRenderState();
|
|
38
|
+
getRenderStateRuntime(state).currentFrameId = 5;
|
|
39
|
+
const obj = createDisplayObject();
|
|
40
|
+
const data = createRenderProxy2D(state, obj);
|
|
41
|
+
updateRenderProxy2DTransform(state, data);
|
|
42
|
+
expect(data.transformFrameId).toBe(5);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('multiplies parent transform into result', () => {
|
|
46
|
+
const state = createRenderState();
|
|
47
|
+
const parentObj = createDisplayObject();
|
|
48
|
+
parentObj.x = 10;
|
|
49
|
+
const parentData = createRenderProxy2D(state, parentObj);
|
|
50
|
+
updateRenderProxy2DTransform(state, parentData);
|
|
51
|
+
parentData.transformFrameId = getRenderStateRuntime(state).currentFrameId;
|
|
52
|
+
|
|
53
|
+
const obj = createDisplayObject();
|
|
54
|
+
obj.x = 5;
|
|
55
|
+
const data = createRenderProxy2D(state, obj);
|
|
56
|
+
updateRenderProxy2DTransform(state, data, parentData);
|
|
57
|
+
expect(data.transform2D.tx).toBeCloseTo(15);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createBitmap, createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix, createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { addNodeChild, getNodeRuntime, invalidateNodeLocalBounds, invalidateNodeLocalTransform } from '@flighthq/node';
|
|
4
|
+
import type { HasBoundsRectangleRuntime, RenderProxy2D, RenderViewport2D } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { createRenderProxy2D, prepareDisplayObjectRender } from './renderProxy';
|
|
7
|
+
import { createRenderState } from './renderState';
|
|
8
|
+
import {
|
|
9
|
+
computeRenderProxyWorldBounds,
|
|
10
|
+
createRenderViewport2D,
|
|
11
|
+
isRenderableInViewport,
|
|
12
|
+
isRenderProxyInViewport,
|
|
13
|
+
} from './renderViewport';
|
|
14
|
+
|
|
15
|
+
function makeMinimalProxy(): RenderProxy2D {
|
|
16
|
+
const state = createRenderState();
|
|
17
|
+
const proxy = createRenderProxy2D(state, createDisplayObject());
|
|
18
|
+
proxy.kind = 'Fake';
|
|
19
|
+
proxy.source = { kind: 'Fake' } as unknown as RenderProxy2D['source'];
|
|
20
|
+
return proxy;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeObjectWithBounds(x: number, y: number, width: number, height: number) {
|
|
24
|
+
const obj = createDisplayObject();
|
|
25
|
+
obj.x = x;
|
|
26
|
+
obj.y = y;
|
|
27
|
+
invalidateNodeLocalTransform(obj);
|
|
28
|
+
const runtime = getNodeRuntime(obj) as unknown as HasBoundsRectangleRuntime;
|
|
29
|
+
runtime.computeLocalBoundsRectangle = (out) => {
|
|
30
|
+
out.x = 0;
|
|
31
|
+
out.y = 0;
|
|
32
|
+
out.width = width;
|
|
33
|
+
out.height = height;
|
|
34
|
+
};
|
|
35
|
+
invalidateNodeLocalBounds(obj);
|
|
36
|
+
return obj;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('computeRenderProxyWorldBounds', () => {
|
|
40
|
+
it('returns false for a source without spatial traits', () => {
|
|
41
|
+
const proxy = makeMinimalProxy();
|
|
42
|
+
const out = createRectangle();
|
|
43
|
+
const result = computeRenderProxyWorldBounds(out, proxy.source);
|
|
44
|
+
expect(result).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns true and writes world bounds for a DisplayObject', () => {
|
|
48
|
+
const obj = makeObjectWithBounds(10, 20, 50, 30);
|
|
49
|
+
const out = createRectangle();
|
|
50
|
+
const result = computeRenderProxyWorldBounds(out, obj);
|
|
51
|
+
expect(result).toBe(true);
|
|
52
|
+
expect(out.x).toBe(10);
|
|
53
|
+
expect(out.y).toBe(20);
|
|
54
|
+
expect(out.width).toBe(50);
|
|
55
|
+
expect(out.height).toBe(30);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not modify out when source has no spatial traits', () => {
|
|
59
|
+
const source = { kind: 'NoTransform' } as any;
|
|
60
|
+
const out = { x: 10, y: 20, width: 30, height: 40 };
|
|
61
|
+
computeRenderProxyWorldBounds(out, source);
|
|
62
|
+
expect(out.x).toBe(10);
|
|
63
|
+
expect(out.y).toBe(20);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('accounts for ancestor transforms in world bounds', () => {
|
|
67
|
+
const parent = createDisplayObject();
|
|
68
|
+
parent.x = 100;
|
|
69
|
+
invalidateNodeLocalTransform(parent);
|
|
70
|
+
const child = makeObjectWithBounds(0, 0, 50, 50);
|
|
71
|
+
addNodeChild(parent, child);
|
|
72
|
+
|
|
73
|
+
const out = createRectangle();
|
|
74
|
+
computeRenderProxyWorldBounds(out, child);
|
|
75
|
+
expect(out.x).toBe(100);
|
|
76
|
+
expect(out.width).toBe(50);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('createRenderViewport2D', () => {
|
|
81
|
+
it('creates a viewport with the given dimensions', () => {
|
|
82
|
+
const vp = createRenderViewport2D(10, 20, 800, 600);
|
|
83
|
+
expect(vp.x).toBe(10);
|
|
84
|
+
expect(vp.y).toBe(20);
|
|
85
|
+
expect(vp.width).toBe(800);
|
|
86
|
+
expect(vp.height).toBe(600);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('isRenderableInViewport', () => {
|
|
91
|
+
it('returns true for a source without spatial traits (conservative)', () => {
|
|
92
|
+
const source = { kind: 'Fake' } as any;
|
|
93
|
+
const vp: RenderViewport2D = { x: 0, y: 0, width: 100, height: 100 };
|
|
94
|
+
expect(isRenderableInViewport(source, vp)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns true for a display object within the viewport', () => {
|
|
98
|
+
const obj = makeObjectWithBounds(10, 10, 50, 30);
|
|
99
|
+
const vp: RenderViewport2D = { x: 0, y: 0, width: 800, height: 600 };
|
|
100
|
+
expect(isRenderableInViewport(obj, vp)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns false for a display object whose world bounds are outside the viewport', () => {
|
|
104
|
+
const obj = makeObjectWithBounds(100, 100, 50, 50);
|
|
105
|
+
const vp: RenderViewport2D = { x: 2000, y: 2000, width: 100, height: 100 };
|
|
106
|
+
expect(isRenderableInViewport(obj, vp)).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('uses world bounds for a nested display object', () => {
|
|
110
|
+
const parent = createDisplayObject();
|
|
111
|
+
parent.x = 500;
|
|
112
|
+
parent.y = 500;
|
|
113
|
+
invalidateNodeLocalTransform(parent);
|
|
114
|
+
const child = makeObjectWithBounds(0, 0, 20, 20);
|
|
115
|
+
addNodeChild(parent, child);
|
|
116
|
+
|
|
117
|
+
const vp: RenderViewport2D = { x: 0, y: 0, width: 100, height: 100 };
|
|
118
|
+
expect(isRenderableInViewport(child, vp)).toBe(false);
|
|
119
|
+
|
|
120
|
+
const largeVp: RenderViewport2D = { x: 0, y: 0, width: 600, height: 600 };
|
|
121
|
+
expect(isRenderableInViewport(child, largeVp)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('culls an object at origin when the viewport is far away', () => {
|
|
125
|
+
const obj = makeObjectWithBounds(0, 0, 30, 30);
|
|
126
|
+
const vp: RenderViewport2D = { x: 5000, y: 5000, width: 100, height: 100 };
|
|
127
|
+
expect(isRenderableInViewport(obj, vp)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('includes an object exactly at the viewport edge (inclusive)', () => {
|
|
131
|
+
const obj = makeObjectWithBounds(0, 0, 100, 100);
|
|
132
|
+
const vp: RenderViewport2D = { x: 100, y: 100, width: 100, height: 100 };
|
|
133
|
+
expect(isRenderableInViewport(obj, vp)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('applies render transform when provided', () => {
|
|
137
|
+
const obj = makeObjectWithBounds(0, 0, 50, 50);
|
|
138
|
+
const vp: RenderViewport2D = { x: 0, y: 0, width: 60, height: 60 };
|
|
139
|
+
|
|
140
|
+
expect(isRenderableInViewport(obj, vp)).toBe(true);
|
|
141
|
+
|
|
142
|
+
const scale2x = createMatrix(2, 0, 0, 2, 0, 0);
|
|
143
|
+
expect(isRenderableInViewport(obj, vp, scale2x)).toBe(true);
|
|
144
|
+
|
|
145
|
+
const farVp: RenderViewport2D = { x: 0, y: 0, width: 60, height: 60 };
|
|
146
|
+
const offsetTransform = createMatrix(1, 0, 0, 1, 200, 200);
|
|
147
|
+
expect(isRenderableInViewport(obj, farVp, offsetTransform)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('works correctly with null render transform', () => {
|
|
151
|
+
const obj = makeObjectWithBounds(10, 10, 50, 50);
|
|
152
|
+
const vp: RenderViewport2D = { x: 0, y: 0, width: 800, height: 600 };
|
|
153
|
+
expect(isRenderableInViewport(obj, vp, null)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('accounts for scaled parent transform in world bounds', () => {
|
|
157
|
+
const parent = createDisplayObject();
|
|
158
|
+
parent.scaleX = 2;
|
|
159
|
+
parent.scaleY = 2;
|
|
160
|
+
invalidateNodeLocalTransform(parent);
|
|
161
|
+
const child = makeObjectWithBounds(0, 0, 50, 50);
|
|
162
|
+
addNodeChild(parent, child);
|
|
163
|
+
|
|
164
|
+
const vp: RenderViewport2D = { x: 80, y: 80, width: 50, height: 50 };
|
|
165
|
+
expect(isRenderableInViewport(child, vp)).toBe(true);
|
|
166
|
+
|
|
167
|
+
const smallVp: RenderViewport2D = { x: 200, y: 200, width: 50, height: 50 };
|
|
168
|
+
expect(isRenderableInViewport(child, smallVp)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('isRenderProxyInViewport', () => {
|
|
173
|
+
it('returns true when the proxy source has no spatial traits', () => {
|
|
174
|
+
const proxy = makeMinimalProxy();
|
|
175
|
+
const vp: RenderViewport2D = { x: 0, y: 0, width: 100, height: 100 };
|
|
176
|
+
expect(isRenderProxyInViewport(proxy, vp)).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns true for a proxy backed by a DisplayObject within the viewport', () => {
|
|
180
|
+
const state = createRenderState();
|
|
181
|
+
const obj = makeObjectWithBounds(10, 10, 50, 50);
|
|
182
|
+
prepareDisplayObjectRender(state, obj);
|
|
183
|
+
const proxy = createRenderProxy2D(state, obj);
|
|
184
|
+
const vp: RenderViewport2D = { x: 0, y: 0, width: 800, height: 600 };
|
|
185
|
+
expect(isRenderProxyInViewport(proxy, vp)).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|