@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.
Files changed (70) hide show
  1. package/dist/index.d.ts +14 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +14 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/renderAppearance.d.ts +3 -0
  6. package/dist/renderAppearance.d.ts.map +1 -0
  7. package/dist/renderAppearance.js +35 -0
  8. package/dist/renderAppearance.js.map +1 -0
  9. package/dist/renderCache.d.ts +36 -0
  10. package/dist/renderCache.d.ts.map +1 -0
  11. package/dist/renderCache.js +83 -0
  12. package/dist/renderCache.js.map +1 -0
  13. package/dist/renderColor.d.ts +3 -0
  14. package/dist/renderColor.d.ts.map +1 -0
  15. package/dist/renderColor.js +17 -0
  16. package/dist/renderColor.js.map +1 -0
  17. package/dist/renderMaterial.d.ts +3 -0
  18. package/dist/renderMaterial.d.ts.map +1 -0
  19. package/dist/renderMaterial.js +9 -0
  20. package/dist/renderMaterial.js.map +1 -0
  21. package/dist/renderProxy.d.ts +19 -0
  22. package/dist/renderProxy.d.ts.map +1 -0
  23. package/dist/renderProxy.js +203 -0
  24. package/dist/renderProxy.js.map +1 -0
  25. package/dist/renderProxyAdapter.d.ts +7 -0
  26. package/dist/renderProxyAdapter.d.ts.map +1 -0
  27. package/dist/renderProxyAdapter.js +32 -0
  28. package/dist/renderProxyAdapter.js.map +1 -0
  29. package/dist/renderQueue.d.ts +9 -0
  30. package/dist/renderQueue.d.ts.map +1 -0
  31. package/dist/renderQueue.js +97 -0
  32. package/dist/renderQueue.js.map +1 -0
  33. package/dist/renderState.d.ts +5 -0
  34. package/dist/renderState.d.ts.map +1 -0
  35. package/dist/renderState.js +42 -0
  36. package/dist/renderState.js.map +1 -0
  37. package/dist/renderTarget.d.ts +21 -0
  38. package/dist/renderTarget.d.ts.map +1 -0
  39. package/dist/renderTarget.js +39 -0
  40. package/dist/renderTarget.js.map +1 -0
  41. package/dist/renderTransform2d.d.ts +3 -0
  42. package/dist/renderTransform2d.d.ts.map +1 -0
  43. package/dist/renderTransform2d.js +26 -0
  44. package/dist/renderTransform2d.js.map +1 -0
  45. package/dist/renderViewport.d.ts +6 -0
  46. package/dist/renderViewport.d.ts.map +1 -0
  47. package/dist/renderViewport.js +57 -0
  48. package/dist/renderViewport.js.map +1 -0
  49. package/dist/renderer.d.ts +6 -0
  50. package/dist/renderer.d.ts.map +1 -0
  51. package/dist/renderer.js +24 -0
  52. package/dist/renderer.js.map +1 -0
  53. package/dist/sceneRender.d.ts +4 -0
  54. package/dist/sceneRender.d.ts.map +1 -0
  55. package/dist/sceneRender.js +175 -0
  56. package/dist/sceneRender.js.map +1 -0
  57. package/package.json +48 -0
  58. package/src/renderAppearance.test.ts +109 -0
  59. package/src/renderCache.test.ts +151 -0
  60. package/src/renderColor.test.ts +10 -0
  61. package/src/renderMaterial.test.ts +30 -0
  62. package/src/renderProxy.test.ts +531 -0
  63. package/src/renderProxyAdapter.test.ts +101 -0
  64. package/src/renderQueue.test.ts +210 -0
  65. package/src/renderState.test.ts +93 -0
  66. package/src/renderTarget.test.ts +84 -0
  67. package/src/renderTransform2d.test.ts +59 -0
  68. package/src/renderViewport.test.ts +187 -0
  69. package/src/renderer.test.ts +128 -0
  70. package/src/sceneRender.test.ts +207 -0
@@ -0,0 +1,531 @@
1
+ import { createDisplayObject, setDisplayObjectClip } from '@flighthq/displayobject';
2
+ import { createMatrix, createRectangle } from '@flighthq/geometry';
3
+ import {
4
+ addNodeChild,
5
+ getNodeAppearanceRevision,
6
+ getNodeLocalContentRevision,
7
+ getNodeLocalTransformRevision,
8
+ invalidateNodeAppearance,
9
+ invalidateNodeLocalTransform,
10
+ } from '@flighthq/node';
11
+ import { createSprite } from '@flighthq/sprite';
12
+ import type { ClipRegion } from '@flighthq/types';
13
+
14
+ import { registerRenderer } from './renderer';
15
+ import {
16
+ createRenderProxy,
17
+ createRenderProxy2D,
18
+ disposeDisplayObjectRender,
19
+ disposeRenderProxy,
20
+ getOrCreateRenderProxy2D,
21
+ getRenderProxy2D,
22
+ installRenderAdaptHook,
23
+ isRenderProxyDirty,
24
+ isRenderProxyVisible,
25
+ prepareDisplayObjectRender,
26
+ updateNodeClip,
27
+ updateRenderProxy2D,
28
+ updateRenderProxyRenderer,
29
+ walkNode,
30
+ } from './renderProxy';
31
+ import { createRenderState, getRenderStateRuntime } from './renderState';
32
+
33
+ const DisplayObjectKind = 'DisplayObject';
34
+
35
+ function makeSource() {
36
+ return { kind: DisplayObjectKind } as any;
37
+ }
38
+
39
+ function makeRenderer() {
40
+ return { createData: () => ({ tag: 'data' }), submit: vi.fn() };
41
+ }
42
+
43
+ function makeClipRegion(): ClipRegion {
44
+ return { contours: null, rect: createRectangle(0, 0, 10, 10), winding: 'nonZero', version: 0 };
45
+ }
46
+
47
+ describe('createRenderProxy', () => {
48
+ it('initializes default values', () => {
49
+ const state = createRenderState();
50
+ const source = makeSource();
51
+ const node = createRenderProxy(state, source);
52
+ expect(node.source).toBe(source);
53
+ expect(node.kind).toBe(source.kind);
54
+ expect(node.next).toBeNull();
55
+ expect(node.alpha).toBe(1);
56
+ expect(node.appearanceFrameId).toBe(-1);
57
+ expect(node.lastAppearanceId).toBe(-1);
58
+ expect(node.lastLocalTransformId).toBe(-1);
59
+ expect(node.transformFrameId).toBe(-1);
60
+ expect(node.renderer).toBeNull();
61
+ expect(node.rendererData).toBeNull();
62
+ expect(node.rendererDataSource).toBe(source);
63
+ expect(node.visible).toBe(true);
64
+ });
65
+
66
+ it('picks up a registered renderer', () => {
67
+ const state = createRenderState();
68
+ const source = makeSource();
69
+ const renderer = makeRenderer();
70
+ registerRenderer(state, source.kind, renderer as any);
71
+ const node = createRenderProxy(state, source);
72
+ expect(node.renderer).toBe(renderer);
73
+ expect(node.rendererData).toEqual({ tag: 'data' });
74
+ });
75
+
76
+ it('uses null renderer when kind has no registration', () => {
77
+ const state = createRenderState();
78
+ const source = makeSource();
79
+ const node = createRenderProxy(state, source);
80
+ expect(node.renderer).toBeNull();
81
+ });
82
+ });
83
+
84
+ describe('createRenderProxy2D', () => {
85
+ it('includes a transform2D matrix', () => {
86
+ const state = createRenderState();
87
+ const obj = createDisplayObject();
88
+ const node = createRenderProxy2D(state, obj);
89
+ expect(node.transform2D).toBeDefined();
90
+ expect(typeof node.transform2D.a).toBe('number');
91
+ });
92
+
93
+ it('sets source to the provided object', () => {
94
+ const state = createRenderState();
95
+ const obj = createDisplayObject();
96
+ const node = createRenderProxy2D(state, obj);
97
+ expect(node.source).toBe(obj);
98
+ });
99
+
100
+ it('initializes the 2D render-node fields the same way for sprites and display objects', () => {
101
+ const state = createRenderState();
102
+ const objNode = createRenderProxy2D(state, createDisplayObject());
103
+ const spriteNode = createRenderProxy2D(state, createSprite());
104
+ for (const node of [objNode, spriteNode]) {
105
+ expect(node.clipDepth).toBe(0);
106
+ expect(node.traverseChildren).toBe(true);
107
+ }
108
+ });
109
+ });
110
+
111
+ describe('disposeDisplayObjectRender', () => {
112
+ it('disposes the root and all descendants', () => {
113
+ const state = createRenderState();
114
+ const root = createDisplayObject();
115
+ const child = createDisplayObject();
116
+ const grandchild = createDisplayObject();
117
+ addNodeChild(root, child);
118
+ addNodeChild(child, grandchild);
119
+ prepareDisplayObjectRender(state, root);
120
+
121
+ disposeDisplayObjectRender(state, root);
122
+
123
+ expect(getRenderProxy2D(state, root)).toBeUndefined();
124
+ expect(getRenderProxy2D(state, child)).toBeUndefined();
125
+ expect(getRenderProxy2D(state, grandchild)).toBeUndefined();
126
+ });
127
+
128
+ it('disposes a sprite subtree the same way', () => {
129
+ const state = createRenderState();
130
+ const root = createSprite();
131
+ const child = createSprite();
132
+ const grandchild = createSprite();
133
+ addNodeChild(root, child);
134
+ addNodeChild(child, grandchild);
135
+ prepareDisplayObjectRender(state, root);
136
+
137
+ disposeDisplayObjectRender(state, root);
138
+
139
+ expect(getRenderProxy2D(state, root)).toBeUndefined();
140
+ expect(getRenderProxy2D(state, child)).toBeUndefined();
141
+ expect(getRenderProxy2D(state, grandchild)).toBeUndefined();
142
+ });
143
+
144
+ it('calls destroyData on renderer data for each disposed node', () => {
145
+ const state = createRenderState();
146
+ const root = createDisplayObject();
147
+ const child = createDisplayObject();
148
+ addNodeChild(root, child);
149
+ const destroyData = vi.fn();
150
+ registerRenderer(state, root.kind, { createData: () => ({ tag: 'data' }), destroyData, submit: vi.fn() } as any);
151
+ getOrCreateRenderProxy2D(state, root);
152
+ getOrCreateRenderProxy2D(state, child);
153
+
154
+ disposeDisplayObjectRender(state, root);
155
+
156
+ expect(destroyData).toHaveBeenCalledTimes(2);
157
+ });
158
+
159
+ it('visits disabled nodes that were never prepared', () => {
160
+ const state = createRenderState();
161
+ const root = createDisplayObject();
162
+ const child = createDisplayObject();
163
+ addNodeChild(root, child);
164
+ child.enabled = false;
165
+ prepareDisplayObjectRender(state, root);
166
+ // prepareDisplayObjectRender skips disabled nodes, but disposeDisplayObjectRender should not
167
+ getOrCreateRenderProxy2D(state, child);
168
+
169
+ disposeDisplayObjectRender(state, root);
170
+
171
+ expect(getRenderProxy2D(state, child)).toBeUndefined();
172
+ });
173
+ });
174
+
175
+ describe('disposeRenderProxy', () => {
176
+ it('cascades to the renderer destroyData and removes the proxy', () => {
177
+ const state = createRenderState();
178
+ const source = createSprite();
179
+ const destroyData = vi.fn();
180
+ registerRenderer(state, source.kind, { createData: () => ({ tag: 'data' }), destroyData, submit: vi.fn() } as any);
181
+ const node = getOrCreateRenderProxy2D(state, source);
182
+ const data = node.rendererData;
183
+
184
+ disposeRenderProxy(state, source);
185
+
186
+ expect(destroyData).toHaveBeenCalledWith(state, data);
187
+ expect(getRenderProxy2D(state, source)).toBeUndefined();
188
+ });
189
+
190
+ it('is a no-op when no proxy exists', () => {
191
+ const state = createRenderState();
192
+ expect(() => disposeRenderProxy(state, createSprite())).not.toThrow();
193
+ });
194
+ });
195
+
196
+ describe('getOrCreateRenderProxy2D', () => {
197
+ it('creates a node on first call', () => {
198
+ const state = createRenderState();
199
+ const obj = createDisplayObject();
200
+ const node = getOrCreateRenderProxy2D(state, obj);
201
+ expect(node.source).toBe(obj);
202
+ expect(node.traverseChildren).toBe(true);
203
+ });
204
+
205
+ it('returns the same node on subsequent calls', () => {
206
+ const state = createRenderState();
207
+ const obj = createDisplayObject();
208
+ const a = getOrCreateRenderProxy2D(state, obj);
209
+ const b = getOrCreateRenderProxy2D(state, obj);
210
+ expect(a).toBe(b);
211
+ });
212
+
213
+ it('syncs renderer when rendererMapId has changed', () => {
214
+ const state = createRenderState();
215
+ const source = createSprite();
216
+ const node = getOrCreateRenderProxy2D(state, source);
217
+ expect(node.renderer).toBeNull();
218
+
219
+ const renderer = makeRenderer();
220
+ registerRenderer(state, source.kind, renderer as any);
221
+ getOrCreateRenderProxy2D(state, source);
222
+
223
+ expect(node.renderer).toBe(renderer);
224
+ });
225
+ });
226
+
227
+ describe('getRenderProxy2D', () => {
228
+ it('returns undefined when no node has been created', () => {
229
+ const state = createRenderState();
230
+ const obj = createDisplayObject();
231
+ expect(getRenderProxy2D(state, obj)).toBeUndefined();
232
+ });
233
+
234
+ it('returns the node after getOrCreateRenderProxy2D', () => {
235
+ const state = createRenderState();
236
+ const obj = createDisplayObject();
237
+ const created = getOrCreateRenderProxy2D(state, obj);
238
+ expect(getRenderProxy2D(state, obj)).toBe(created);
239
+ });
240
+ });
241
+
242
+ describe('installRenderAdaptHook', () => {
243
+ it('does not throw when installing a hook', () => {
244
+ const state = createRenderState();
245
+ expect(() => installRenderAdaptHook(state, vi.fn())).not.toThrow();
246
+ });
247
+
248
+ it('writes the hook to the render state runtime', () => {
249
+ const state = createRenderState();
250
+ const hook = vi.fn();
251
+ installRenderAdaptHook(state, hook);
252
+ expect(getRenderStateRuntime(state).renderAdaptHook).toBe(hook);
253
+ });
254
+ });
255
+
256
+ describe('isRenderProxyDirty', () => {
257
+ it('returns false when source and parent are clean', () => {
258
+ const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
259
+ const source = createDisplayObject();
260
+ const data = createRenderProxy2D(state, source);
261
+ data.lastAppearanceId = getNodeAppearanceRevision(source);
262
+ data.lastLocalContentId = getNodeLocalContentRevision(source);
263
+ data.lastLocalTransformId = getNodeLocalTransformRevision(source);
264
+
265
+ expect(isRenderProxyDirty(state, source, data)).toBe(false);
266
+ });
267
+
268
+ it('returns true when appearance changes', () => {
269
+ const state = createRenderState();
270
+ const source = createDisplayObject();
271
+ const data = createRenderProxy2D(state, source);
272
+ data.lastAppearanceId = getNodeAppearanceRevision(source);
273
+ data.lastLocalTransformId = getNodeLocalTransformRevision(source);
274
+ invalidateNodeAppearance(source);
275
+
276
+ expect(isRenderProxyDirty(state, source, data)).toBe(true);
277
+ });
278
+
279
+ it('returns true when parent was updated this frame', () => {
280
+ const state = createRenderState();
281
+ const source = createDisplayObject();
282
+ const data = createRenderProxy2D(state, source);
283
+ data.lastAppearanceId = getNodeAppearanceRevision(source);
284
+ data.lastLocalTransformId = getNodeLocalTransformRevision(source);
285
+ const parentData = createRenderProxy2D(state, createDisplayObject());
286
+ parentData.transformFrameId = getRenderStateRuntime(state).currentFrameId;
287
+
288
+ expect(isRenderProxyDirty(state, source, data, parentData)).toBe(true);
289
+ });
290
+
291
+ it('returns true when transform changes', () => {
292
+ const state = createRenderState();
293
+ const source = createDisplayObject();
294
+ const data = createRenderProxy2D(state, source);
295
+ data.transform2D = createMatrix();
296
+ data.lastAppearanceId = getNodeAppearanceRevision(source);
297
+ data.lastLocalTransformId = getNodeLocalTransformRevision(source);
298
+ source.x = 10;
299
+ invalidateNodeLocalTransform(source);
300
+
301
+ expect(isRenderProxyDirty(state, source, data)).toBe(true);
302
+ });
303
+ });
304
+
305
+ describe('isRenderProxyVisible', () => {
306
+ it('returns false when alpha is zero', () => {
307
+ const state = createRenderState();
308
+ const node = createRenderProxy2D(state, createDisplayObject());
309
+ node.alpha = 0;
310
+
311
+ expect(isRenderProxyVisible(node)).toBe(false);
312
+ });
313
+
314
+ it('returns false when the node is hidden', () => {
315
+ const state = createRenderState();
316
+ const node = createRenderProxy2D(state, createDisplayObject());
317
+ node.visible = false;
318
+
319
+ expect(isRenderProxyVisible(node)).toBe(false);
320
+ });
321
+
322
+ it('returns false when the transform collapses both axes', () => {
323
+ const state = createRenderState();
324
+ const node = createRenderProxy2D(state, createDisplayObject());
325
+ node.transform2D.a = 0;
326
+ node.transform2D.d = 0;
327
+
328
+ expect(isRenderProxyVisible(node)).toBe(false);
329
+ });
330
+
331
+ it('returns true for a visible node with positive alpha and non-collapsed transform', () => {
332
+ const state = createRenderState();
333
+ const node = createRenderProxy2D(state, createDisplayObject());
334
+
335
+ expect(isRenderProxyVisible(node)).toBe(true);
336
+ });
337
+ });
338
+
339
+ describe('prepareDisplayObjectRender', () => {
340
+ it('creates render nodes for all enabled nodes in the tree', () => {
341
+ const state = createRenderState();
342
+ const root = createDisplayObject();
343
+ const child = createDisplayObject();
344
+ addNodeChild(root, child);
345
+
346
+ prepareDisplayObjectRender(state, root);
347
+
348
+ expect(getRenderStateRuntime(state).renderProxyMap.get(root)).toBeDefined();
349
+ expect(getRenderStateRuntime(state).renderProxyMap.get(child)).toBeDefined();
350
+ });
351
+
352
+ it('prepares a sprite tree the same way', () => {
353
+ const state = createRenderState();
354
+ const root = createSprite();
355
+ const child = createSprite();
356
+ addNodeChild(root, child);
357
+
358
+ prepareDisplayObjectRender(state, root);
359
+
360
+ expect(getRenderStateRuntime(state).renderProxyMap.get(root)).toBeDefined();
361
+ expect(getRenderStateRuntime(state).renderProxyMap.get(child)).toBeDefined();
362
+ });
363
+
364
+ it('skips disabled nodes', () => {
365
+ const state = createRenderState();
366
+ const root = createDisplayObject();
367
+ const child = createDisplayObject();
368
+ addNodeChild(root, child);
369
+ child.enabled = false;
370
+
371
+ prepareDisplayObjectRender(state, root);
372
+
373
+ expect(getRenderStateRuntime(state).renderProxyMap.get(root)).toBeDefined();
374
+ expect(getRenderStateRuntime(state).renderProxyMap.get(child)).toBeUndefined();
375
+ });
376
+
377
+ it('returns true when tree is dirty', () => {
378
+ const state = createRenderState();
379
+ const root = createDisplayObject();
380
+
381
+ expect(prepareDisplayObjectRender(state, root)).toBe(true);
382
+ });
383
+
384
+ it('returns false when tree is clean', () => {
385
+ const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
386
+ const root = createDisplayObject();
387
+ prepareDisplayObjectRender(state, root);
388
+
389
+ expect(prepareDisplayObjectRender(state, root)).toBe(false);
390
+ });
391
+
392
+ it('accumulates clip depth down a clipped subtree', () => {
393
+ const state = createRenderState();
394
+ const root = createDisplayObject();
395
+ const child = createDisplayObject();
396
+ const grandchild = createDisplayObject();
397
+ addNodeChild(root, child);
398
+ addNodeChild(child, grandchild);
399
+ setDisplayObjectClip(root, makeClipRegion());
400
+ setDisplayObjectClip(child, makeClipRegion());
401
+
402
+ prepareDisplayObjectRender(state, root);
403
+
404
+ expect(getOrCreateRenderProxy2D(state, root).clipDepth).toBe(1);
405
+ expect(getOrCreateRenderProxy2D(state, child).clipDepth).toBe(2);
406
+ expect(getOrCreateRenderProxy2D(state, grandchild).clipDepth).toBe(2);
407
+ });
408
+ });
409
+
410
+ describe('updateNodeClip', () => {
411
+ it('adds one to the parent depth when the node has a clip', () => {
412
+ const state = createRenderState();
413
+ const node = createDisplayObject();
414
+ setDisplayObjectClip(node, makeClipRegion());
415
+ const data = getOrCreateRenderProxy2D(state, node);
416
+ updateNodeClip(state, node, data, undefined);
417
+ expect(data.clipDepth).toBe(1);
418
+ });
419
+
420
+ it('inherits the parent depth when the node has no clip', () => {
421
+ const state = createRenderState();
422
+ const node = createDisplayObject();
423
+ const data = getOrCreateRenderProxy2D(state, node);
424
+ const parentData = getOrCreateRenderProxy2D(state, createDisplayObject());
425
+ parentData.clipDepth = 2;
426
+ updateNodeClip(state, node, data, parentData);
427
+ expect(data.clipDepth).toBe(2);
428
+ });
429
+
430
+ it('contributes no depth for a sprite node that lacks the clip trait', () => {
431
+ const state = createRenderState();
432
+ const sprite = createSprite();
433
+ const data = getOrCreateRenderProxy2D(state, sprite);
434
+ updateNodeClip(state, sprite, data, undefined);
435
+ expect(data.clipDepth).toBe(0);
436
+ });
437
+ });
438
+
439
+ describe('updateRenderProxy2D', () => {
440
+ it('updates appearance on a display-object render node', () => {
441
+ const state = createRenderState();
442
+ const root = createDisplayObject();
443
+ const data = getOrCreateRenderProxy2D(state, root);
444
+ updateRenderProxy2D(state, root, data, undefined);
445
+ expect(data.alpha).toBe(1);
446
+ });
447
+
448
+ it('updates appearance on a sprite render node through the same visitor', () => {
449
+ const state = createRenderState();
450
+ const root = createSprite();
451
+ const data = getOrCreateRenderProxy2D(state, root);
452
+ updateRenderProxy2D(state, root, data, undefined);
453
+ expect(data.alpha).toBe(1);
454
+ });
455
+
456
+ it('sets clip depth from the parent through the same visitor', () => {
457
+ const state = createRenderState();
458
+ const root = createDisplayObject();
459
+ setDisplayObjectClip(root, makeClipRegion());
460
+ const data = getOrCreateRenderProxy2D(state, root);
461
+ updateRenderProxy2D(state, root, data, undefined);
462
+ expect(data.clipDepth).toBe(1);
463
+ });
464
+ });
465
+
466
+ describe('updateRenderProxyRenderer', () => {
467
+ it('sets renderer from the map matching the node kind', () => {
468
+ const state = createRenderState();
469
+ const source = makeSource();
470
+ const renderer = makeRenderer();
471
+ registerRenderer(state, source.kind, renderer as any);
472
+ const node = createRenderProxy(state, source);
473
+ node.renderer = null;
474
+ updateRenderProxyRenderer(state, node);
475
+ expect(node.renderer).toBe(renderer);
476
+ });
477
+
478
+ it('sets renderer to null when no registration matches', () => {
479
+ const state = createRenderState();
480
+ const source = makeSource();
481
+ const node = createRenderProxy(state, source);
482
+ node.renderer = { createData: vi.fn(), submit: vi.fn() } as any;
483
+ updateRenderProxyRenderer(state, node);
484
+ expect(node.renderer).toBeNull();
485
+ });
486
+
487
+ it('updates rendererMapId to current state value', () => {
488
+ const state = createRenderState();
489
+ const source = makeSource();
490
+ const node = createRenderProxy(state, source);
491
+ node.rendererMapId = -1;
492
+ updateRenderProxyRenderer(state, node);
493
+ expect(node.rendererMapId).toBe(getRenderStateRuntime(state).rendererMapId);
494
+ });
495
+
496
+ it('updates rendererData when source changes', () => {
497
+ const state = createRenderState();
498
+ const source = makeSource();
499
+ const renderer = makeRenderer();
500
+ registerRenderer(state, source.kind, renderer as any);
501
+ const node = createRenderProxy(state, source);
502
+ const newSource = makeSource();
503
+ node.source = newSource;
504
+ updateRenderProxyRenderer(state, node);
505
+ expect(node.rendererDataSource).toBe(newSource);
506
+ });
507
+ });
508
+
509
+ describe('walkNode', () => {
510
+ it('calls the visitor for each enabled node and reports dirty', () => {
511
+ const state = createRenderState();
512
+ const root = createSprite();
513
+ const child = createSprite();
514
+ addNodeChild(root, child);
515
+ const visit = vi.fn();
516
+ const dirty = walkNode(state, root, visit);
517
+ expect(visit).toHaveBeenCalledTimes(2);
518
+ expect(dirty).toBe(true);
519
+ });
520
+
521
+ it('skips a node whose render node has traverseChildren false', () => {
522
+ const state = createRenderState();
523
+ const root = createSprite();
524
+ const child = createSprite();
525
+ addNodeChild(root, child);
526
+ getOrCreateRenderProxy2D(state, root).traverseChildren = false;
527
+ const visit = vi.fn();
528
+ walkNode(state, root, visit);
529
+ expect(getRenderStateRuntime(state).renderProxyMap.get(child)).toBeUndefined();
530
+ });
531
+ });
@@ -0,0 +1,101 @@
1
+ import { createDisplayObject } from '@flighthq/displayobject';
2
+ import type { RenderProxyAdapter } from '@flighthq/types';
3
+
4
+ import { registerRenderer } from './renderer';
5
+ import { createRenderProxy2D } from './renderProxy';
6
+ import { applyRenderProxyAdapter, getRenderProxyAdapter, setRenderProxyAdapter } from './renderProxyAdapter';
7
+ import { createRenderState } from './renderState';
8
+
9
+ describe('applyRenderProxyAdapter', () => {
10
+ it('sets traverseChildren to true when no resolver is attached', () => {
11
+ const state = createRenderState();
12
+ const source = createDisplayObject();
13
+ const data = createRenderProxy2D(state, source);
14
+ data.traverseChildren = false;
15
+
16
+ applyRenderProxyAdapter(state, source, data);
17
+
18
+ expect(data.traverseChildren).toBe(true);
19
+ });
20
+
21
+ it('sets traverseChildren from a non-null adapter result', () => {
22
+ const state = createRenderState();
23
+ const source = createDisplayObject();
24
+ const data = createRenderProxy2D(state, source);
25
+ const adapter: RenderProxyAdapter = {
26
+ adapt: vi.fn().mockReturnValue(false),
27
+ };
28
+ setRenderProxyAdapter(state, source, adapter);
29
+
30
+ applyRenderProxyAdapter(state, source, data);
31
+
32
+ expect(data.traverseChildren).toBe(false);
33
+ });
34
+
35
+ it('syncs renderer when the adapter changes the node kind', () => {
36
+ const state = createRenderState();
37
+ const source = createDisplayObject();
38
+ const data = createRenderProxy2D(state, source);
39
+ const kind = 'Adapted';
40
+ const renderer = { createData: () => null, submit: vi.fn() };
41
+ registerRenderer(state, kind, renderer);
42
+ const adapter: RenderProxyAdapter = {
43
+ adapt: (_state, _source, node) => {
44
+ node.kind = kind;
45
+ return true;
46
+ },
47
+ };
48
+ setRenderProxyAdapter(state, source, adapter);
49
+
50
+ applyRenderProxyAdapter(state, source, data);
51
+
52
+ expect(data.renderer).toBe(renderer);
53
+ });
54
+ });
55
+
56
+ describe('getRenderProxyAdapter', () => {
57
+ it('returns null when no adapter is set', () => {
58
+ const state = createRenderState();
59
+ const source = createDisplayObject();
60
+ expect(getRenderProxyAdapter(state, source)).toBeNull();
61
+ });
62
+
63
+ it('returns the adapter after setRenderProxyAdapter', () => {
64
+ const state = createRenderState();
65
+ const source = createDisplayObject();
66
+ const adapter: RenderProxyAdapter = { adapt: vi.fn() };
67
+ setRenderProxyAdapter(state, source, adapter);
68
+ expect(getRenderProxyAdapter(state, source)).toBe(adapter);
69
+ setRenderProxyAdapter(state, source, null);
70
+ });
71
+ });
72
+
73
+ describe('setRenderProxyAdapter', () => {
74
+ it('sets an adapter for the source', () => {
75
+ const state = createRenderState();
76
+ const source = createDisplayObject();
77
+ const adapter: RenderProxyAdapter = { adapt: vi.fn() };
78
+ setRenderProxyAdapter(state, source, adapter);
79
+ expect(getRenderProxyAdapter(state, source)).toBe(adapter);
80
+ setRenderProxyAdapter(state, source, null);
81
+ });
82
+
83
+ it('isolates adapters between render states', () => {
84
+ const stateA = createRenderState();
85
+ const stateB = createRenderState();
86
+ const source = createDisplayObject();
87
+ const adapter: RenderProxyAdapter = { adapt: vi.fn() };
88
+ setRenderProxyAdapter(stateA, source, adapter);
89
+ expect(getRenderProxyAdapter(stateA, source)).toBe(adapter);
90
+ expect(getRenderProxyAdapter(stateB, source)).toBeNull();
91
+ });
92
+
93
+ it('removes the adapter when passed null', () => {
94
+ const state = createRenderState();
95
+ const source = createDisplayObject();
96
+ const adapter: RenderProxyAdapter = { adapt: vi.fn() };
97
+ setRenderProxyAdapter(state, source, adapter);
98
+ setRenderProxyAdapter(state, source, null);
99
+ expect(getRenderProxyAdapter(state, source)).toBeNull();
100
+ });
101
+ });