@flighthq/node 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 (75) hide show
  1. package/dist/boundsRectangle.d.ts +26 -0
  2. package/dist/boundsRectangle.d.ts.map +1 -0
  3. package/dist/boundsRectangle.js +160 -0
  4. package/dist/boundsRectangle.js.map +1 -0
  5. package/dist/hasAppearance.d.ts +3 -0
  6. package/dist/hasAppearance.d.ts.map +1 -0
  7. package/dist/hasAppearance.js +6 -0
  8. package/dist/hasAppearance.js.map +1 -0
  9. package/dist/hasBoundsRectangle.d.ts +5 -0
  10. package/dist/hasBoundsRectangle.d.ts.map +1 -0
  11. package/dist/hasBoundsRectangle.js +9 -0
  12. package/dist/hasBoundsRectangle.js.map +1 -0
  13. package/dist/hasClip.d.ts +3 -0
  14. package/dist/hasClip.d.ts.map +1 -0
  15. package/dist/hasClip.js +4 -0
  16. package/dist/hasClip.js.map +1 -0
  17. package/dist/hasMaterial.d.ts +3 -0
  18. package/dist/hasMaterial.d.ts.map +1 -0
  19. package/dist/hasMaterial.js +5 -0
  20. package/dist/hasMaterial.js.map +1 -0
  21. package/dist/hasTransform2d.d.ts +4 -0
  22. package/dist/hasTransform2d.d.ts.map +1 -0
  23. package/dist/hasTransform2d.js +19 -0
  24. package/dist/hasTransform2d.js.map +1 -0
  25. package/dist/hasTransform3d.d.ts +5 -0
  26. package/dist/hasTransform3d.d.ts.map +1 -0
  27. package/dist/hasTransform3d.js +8 -0
  28. package/dist/hasTransform3d.js.map +1 -0
  29. package/dist/hierarchy.d.ts +127 -0
  30. package/dist/hierarchy.d.ts.map +1 -0
  31. package/dist/hierarchy.js +396 -0
  32. package/dist/hierarchy.js.map +1 -0
  33. package/dist/index.d.ts +15 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +15 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/node.d.ts +21 -0
  38. package/dist/node.d.ts.map +1 -0
  39. package/dist/node.js +109 -0
  40. package/dist/node.js.map +1 -0
  41. package/dist/revision.d.ts +40 -0
  42. package/dist/revision.d.ts.map +1 -0
  43. package/dist/revision.js +85 -0
  44. package/dist/revision.js.map +1 -0
  45. package/dist/transform2d.d.ts +16 -0
  46. package/dist/transform2d.d.ts.map +1 -0
  47. package/dist/transform2d.js +99 -0
  48. package/dist/transform2d.js.map +1 -0
  49. package/dist/transform3d.d.ts +6 -0
  50. package/dist/transform3d.d.ts.map +1 -0
  51. package/dist/transform3d.js +45 -0
  52. package/dist/transform3d.js.map +1 -0
  53. package/dist/traversal.d.ts +51 -0
  54. package/dist/traversal.d.ts.map +1 -0
  55. package/dist/traversal.js +130 -0
  56. package/dist/traversal.js.map +1 -0
  57. package/dist/viewport.d.ts +8 -0
  58. package/dist/viewport.d.ts.map +1 -0
  59. package/dist/viewport.js +78 -0
  60. package/dist/viewport.js.map +1 -0
  61. package/package.json +40 -0
  62. package/src/boundsRectangle.test.ts +497 -0
  63. package/src/hasAppearance.test.ts +47 -0
  64. package/src/hasBoundsRectangle.test.ts +57 -0
  65. package/src/hasClip.test.ts +27 -0
  66. package/src/hasMaterial.test.ts +30 -0
  67. package/src/hasTransform2d.test.ts +67 -0
  68. package/src/hasTransform3d.test.ts +37 -0
  69. package/src/hierarchy.test.ts +973 -0
  70. package/src/node.test.ts +265 -0
  71. package/src/revision.test.ts +211 -0
  72. package/src/transform2d.test.ts +391 -0
  73. package/src/transform3d.test.ts +202 -0
  74. package/src/traversal.test.ts +233 -0
  75. package/src/viewport.test.ts +228 -0
@@ -0,0 +1,497 @@
1
+ import { getEntityRuntime as _getRuntime } from '@flighthq/entity';
2
+ import {
3
+ cloneRectangle,
4
+ createRectangle,
5
+ equalsRectangle,
6
+ setEmptyRectangle as setEmpty,
7
+ setRectangle,
8
+ } from '@flighthq/geometry';
9
+ import {
10
+ addNodeChild,
11
+ createNode,
12
+ ensureNodeLocalTransformMatrix,
13
+ initTransform2DRuntimeTrait,
14
+ initTransform2DTrait,
15
+ invalidateNodeLocalTransform,
16
+ } from '@flighthq/node';
17
+ import type {
18
+ HasBoundsRectangle,
19
+ HasBoundsRectangleRuntime,
20
+ HasTransform2D,
21
+ HasTransform2DRuntime,
22
+ Node,
23
+ NodeRuntime,
24
+ Rectangle,
25
+ } from '@flighthq/types';
26
+
27
+ import {
28
+ computeNodeBoundsRectangle,
29
+ ensureNodeLocalBoundsRectangle,
30
+ ensureNodeParentBoundsRectangle,
31
+ ensureNodeWorldBoundsRectangle,
32
+ getNodeHeight,
33
+ getNodeLocalBoundsRectangle,
34
+ getNodeParentBoundsRectangle,
35
+ getNodeWidth,
36
+ getNodeWorldBoundsRectangle,
37
+ setNodeHeight,
38
+ setNodeWidth,
39
+ } from './boundsRectangle';
40
+ import { initBoundsRectangleRuntimeTrait, initBoundsRectangleTrait } from './hasBoundsRectangle';
41
+
42
+ function getEntityRuntime(
43
+ source: TestNode,
44
+ ): NodeRuntime<HasBoundsRectangle & HasTransform2D> & HasBoundsRectangleRuntime {
45
+ return _getRuntime(source) as NodeRuntime<HasBoundsRectangle & HasTransform2D> & HasBoundsRectangleRuntime;
46
+ }
47
+
48
+ function createTestNode(): TestNode {
49
+ const node = createNode(TestKind) as TestNode;
50
+ const runtime = _getRuntime(node);
51
+ initBoundsRectangleTrait(node);
52
+ initBoundsRectangleRuntimeTrait(runtime as HasBoundsRectangleRuntime);
53
+ initTransform2DTrait(node);
54
+ initTransform2DRuntimeTrait(runtime as HasTransform2DRuntime);
55
+ return node;
56
+ }
57
+
58
+ describe('computeNodeBoundsRectangle', () => {
59
+ let root: TestNode;
60
+ let child: TestNode;
61
+ let grandChild: TestNode;
62
+
63
+ beforeEach(() => {
64
+ root = createTestNode();
65
+ child = createTestNode();
66
+ grandChild = createTestNode();
67
+
68
+ child.x = 100;
69
+ child.y = 100;
70
+
71
+ addNodeChild(root, child);
72
+ addNodeChild(child, grandChild);
73
+
74
+ // fake local bounds
75
+ setRectangle(getNodeLocalBoundsRectangle(root), 0, 0, 100, 100);
76
+ setRectangle(getNodeLocalBoundsRectangle(child), 10, 20, 50, 50);
77
+ setRectangle(getNodeLocalBoundsRectangle(grandChild), 5, 5, 100, 100);
78
+ });
79
+
80
+ it('should equal local bounds when targetCoordinateSpace is self and there are no children', () => {
81
+ const out = createRectangle();
82
+ computeNodeBoundsRectangle(out, grandChild, grandChild);
83
+ expect(equalsRectangle(out, getNodeLocalBoundsRectangle(grandChild))).toBe(true);
84
+ });
85
+
86
+ it('should include children bounds when targetCoordinateSpace is self', () => {
87
+ const out = createRectangle();
88
+ computeNodeBoundsRectangle(out, child, child);
89
+ expect(equalsRectangle(out, { x: 5, y: 5, width: 100, height: 100 })).toBe(true);
90
+ });
91
+
92
+ it('should compute bounds relative to parent', () => {
93
+ const out = createRectangle();
94
+ computeNodeBoundsRectangle(out, child, root);
95
+ expect(out.x).toBeCloseTo(105);
96
+ expect(out.y).toBeCloseTo(105);
97
+ expect(out.width).toBeCloseTo(100);
98
+ expect(out.height).toBeCloseTo(100);
99
+ });
100
+
101
+ it('should compute bounds relative to nested child', () => {
102
+ const out = createRectangle();
103
+ computeNodeBoundsRectangle(out, root, grandChild);
104
+ expect(out.width).toBeGreaterThan(0);
105
+ expect(out.height).toBeGreaterThan(0);
106
+ // exact numbers depend on transforms
107
+ });
108
+
109
+ it('should account for scaling in parent transforms', () => {
110
+ // child is 50x50, should be 100x150 now in parent coordinate space
111
+ child.scaleX = 2;
112
+ child.scaleY = 3;
113
+
114
+ const out = createRectangle();
115
+ computeNodeBoundsRectangle(out, child, root);
116
+
117
+ expect(out.width).toBeCloseTo(100 * 2);
118
+ expect(out.height).toBeCloseTo(100 * 3);
119
+ });
120
+
121
+ it('should account for translation in parent transforms', () => {
122
+ child.x = 5;
123
+ child.y = 7;
124
+
125
+ const out = createRectangle();
126
+ computeNodeBoundsRectangle(out, grandChild, root);
127
+
128
+ // grandChild localBounds at (5,5) with no scaling
129
+ expect(out.x).toBeCloseTo(5 + 5); // parent offset + grandChild offset
130
+ expect(out.y).toBeCloseTo(7 + 5);
131
+ });
132
+
133
+ it('should handle rotation', () => {
134
+ child.rotation = 90;
135
+
136
+ const out = createRectangle();
137
+ computeNodeBoundsRectangle(out, child, root);
138
+ expect(out.width).toBeCloseTo(100); // roughly unchanged
139
+ expect(out.height).toBeCloseTo(100);
140
+ });
141
+
142
+ it('should allow a rectangle-like object', () => {
143
+ const out = { x: 0, y: 0, width: 0, height: 0 };
144
+ computeNodeBoundsRectangle(out, grandChild, grandChild);
145
+ expect(equalsRectangle(out, getNodeLocalBoundsRectangle(grandChild))).toBe(true);
146
+ });
147
+
148
+ it('should compute bounds relative to an unrelated target', () => {
149
+ const out = createRectangle();
150
+ const unrelatedTarget = createTestNode(); // another object in a separate scene graph
151
+ computeNodeBoundsRectangle(out, child, unrelatedTarget);
152
+ expect(equalsRectangle(out, getNodeWorldBoundsRectangle(child))).toBe(true);
153
+ });
154
+
155
+ it('should return world bounds if the target coordinate space is root', () => {
156
+ const out = createRectangle();
157
+ computeNodeBoundsRectangle(out, child, root);
158
+ expect(equalsRectangle(out, getNodeWorldBoundsRectangle(child))).toBe(true);
159
+ });
160
+ });
161
+
162
+ describe('ensureNodeLocalBoundsRectangle', () => {
163
+ it('should ensure localBoundsRectangle is defined', () => {
164
+ const object = createTestNode();
165
+ const runtime = getEntityRuntime(object);
166
+ expect(runtime.localBoundsRectangle).toBeNull();
167
+ ensureNodeLocalBoundsRectangle(object);
168
+ expect(runtime.localBoundsRectangle).not.toBeNull();
169
+ });
170
+
171
+ it('should not recalculate if localBoundsId is unchanged', () => {
172
+ const object = createTestNode();
173
+ const runtime = getEntityRuntime(object);
174
+ runtime.computeLocalBoundsRectangle = (out: Rectangle, _source: Node) => {
175
+ setEmpty(out);
176
+ };
177
+ ensureNodeLocalBoundsRectangle(object);
178
+ const cache = cloneAndInvalidateRect(runtime.localBoundsRectangle!);
179
+ ensureNodeLocalBoundsRectangle(object);
180
+ expect(runtime.localBoundsRectangle).not.toEqual(cache);
181
+ });
182
+
183
+ it('should recalculate if localBoundsId is changed', () => {
184
+ const object = createTestNode();
185
+ const runtime = getEntityRuntime(object);
186
+ runtime.computeLocalBoundsRectangle = (out: Rectangle, _source: Node) => {
187
+ setEmpty(out);
188
+ };
189
+ ensureNodeLocalBoundsRectangle(object);
190
+ const cache = cloneAndInvalidateRect(runtime.localBoundsRectangle!);
191
+ runtime.localBoundsId++;
192
+ ensureNodeLocalBoundsRectangle(object);
193
+ expect(equalsRectangle(runtime.localBoundsRectangle, cache)).toBe(true);
194
+ });
195
+
196
+ it('should not recalculate if localTransformId is unchanged', () => {
197
+ const object = createTestNode();
198
+ const runtime = getEntityRuntime(object);
199
+ runtime.computeLocalBoundsRectangle = (out: Rectangle, _source: Node) => {
200
+ setEmpty(out);
201
+ };
202
+ ensureNodeLocalBoundsRectangle(object);
203
+ const cache = cloneAndInvalidateRect(runtime.localBoundsRectangle!);
204
+ runtime.localTransformId++;
205
+ ensureNodeLocalBoundsRectangle(object);
206
+ expect(runtime.localBoundsRectangle).not.toEqual(cache);
207
+ });
208
+ });
209
+
210
+ describe('ensureNodeParentBoundsRectangle', () => {
211
+ it('should ensure boundsRectangle is defined', () => {
212
+ const object = createTestNode();
213
+ const runtime = getEntityRuntime(object);
214
+ expect(runtime.boundsRectangle).toBeNull();
215
+ ensureNodeParentBoundsRectangle(object);
216
+ expect(runtime.boundsRectangle).not.toBeNull();
217
+ });
218
+
219
+ it('should not recalculate if localBoundsId and localTransformId are unchanged', () => {
220
+ const object = createTestNode();
221
+ const runtime = getEntityRuntime(object);
222
+ ensureNodeParentBoundsRectangle(object);
223
+ const cache = cloneAndInvalidateRect(runtime.boundsRectangle!);
224
+ ensureNodeParentBoundsRectangle(object);
225
+ expect(runtime.boundsRectangle).not.toEqual(cache);
226
+ });
227
+
228
+ it('should recalculate if localBoundsId is changed', () => {
229
+ const object = createTestNode();
230
+ const runtime = getEntityRuntime(object);
231
+ runtime.computeLocalBoundsRectangle = (out: Rectangle, _source: Node) => {
232
+ setEmpty(out);
233
+ };
234
+ ensureNodeParentBoundsRectangle(object);
235
+ const cache = cloneAndInvalidateRect(runtime.boundsRectangle!);
236
+ runtime.localBoundsId++;
237
+ ensureNodeParentBoundsRectangle(object);
238
+ expect(equalsRectangle(runtime.boundsRectangle, cache)).toBe(true);
239
+ });
240
+
241
+ it('should recalculate if localTransformId is changed', () => {
242
+ const object = createTestNode();
243
+ const runtime = getEntityRuntime(object);
244
+ ensureNodeParentBoundsRectangle(object);
245
+ const cache = cloneAndInvalidateRect(runtime.boundsRectangle!);
246
+ runtime.localTransformId++;
247
+ ensureNodeParentBoundsRectangle(object);
248
+ expect(equalsRectangle(runtime.boundsRectangle, cache)).toBe(true);
249
+ });
250
+ });
251
+
252
+ describe('ensureNodeWorldBoundsRectangle', () => {
253
+ it('should ensure worldBoundsRectangle is defined', () => {
254
+ const object = createTestNode();
255
+ const runtime = getEntityRuntime(object);
256
+ expect(runtime.worldBoundsRectangle).toBeNull();
257
+ ensureNodeWorldBoundsRectangle(object);
258
+ expect(runtime.worldBoundsRectangle).not.toBeNull();
259
+ });
260
+
261
+ it('should not recalculate if localBoundsId and worldTransformId are unchanged', () => {
262
+ const object = createTestNode();
263
+ const runtime = getEntityRuntime(object);
264
+ ensureNodeWorldBoundsRectangle(object);
265
+ const cache = cloneAndInvalidateRect(runtime.worldBoundsRectangle!);
266
+ ensureNodeWorldBoundsRectangle(object);
267
+ expect(runtime.worldBoundsRectangle).not.toEqual(cache);
268
+ });
269
+
270
+ it('should recalculate if localBoundsId is changed', () => {
271
+ const object = createTestNode();
272
+ const runtime = getEntityRuntime(object);
273
+ ensureNodeWorldBoundsRectangle(object);
274
+ const cache = cloneAndInvalidateRect(runtime.worldBoundsRectangle!);
275
+ runtime.localBoundsId++;
276
+ ensureNodeWorldBoundsRectangle(object);
277
+ expect(equalsRectangle(runtime.worldBoundsRectangle, cache)).toBe(true);
278
+ });
279
+
280
+ it('should recalculate if local transform is changed (translate)', () => {
281
+ const object = createTestNode();
282
+ const runtime = getEntityRuntime(object);
283
+ ensureNodeWorldBoundsRectangle(object);
284
+ const cache = cloneRectangle(runtime.worldBoundsRectangle!);
285
+ object.x = 100;
286
+ invalidateNodeLocalTransform(object);
287
+ ensureNodeWorldBoundsRectangle(object);
288
+ expect(runtime.worldBoundsRectangle).not.toEqual(cache);
289
+ expect(equalsRectangle(runtime.worldBoundsRectangle, { x: 100, y: 0, width: 0, height: 0 })).toBe(true);
290
+ });
291
+
292
+ it('should recalculate if local transform is changed (scale)', () => {
293
+ const object = createTestNode();
294
+ const runtime = getEntityRuntime(object);
295
+ ensureNodeWorldBoundsRectangle(object);
296
+ const cache = cloneRectangle(runtime.worldBoundsRectangle!);
297
+ const localBounds = getNodeLocalBoundsRectangle(object) as Rectangle;
298
+ // write the cached rectangle directly (bypassing invalidation) so the scaled recalculation is observable
299
+ localBounds.width = 10;
300
+ object.scaleX = 2;
301
+ invalidateNodeLocalTransform(object);
302
+ ensureNodeWorldBoundsRectangle(object);
303
+ expect(runtime.worldBoundsRectangle).not.toEqual(cache);
304
+ expect(equalsRectangle(runtime.worldBoundsRectangle, { x: 0, y: 0, width: 20, height: 0 })).toBe(true);
305
+ });
306
+
307
+ it('should recalculate if parent transform is changed (translate)', () => {
308
+ const parent = createTestNode();
309
+ const child = createTestNode();
310
+ addNodeChild(parent, child);
311
+ const runtime = getEntityRuntime(child);
312
+ ensureNodeWorldBoundsRectangle(child);
313
+ const cache = cloneRectangle(runtime.worldBoundsRectangle!);
314
+ parent.x = 100;
315
+ invalidateNodeLocalTransform(parent);
316
+ ensureNodeWorldBoundsRectangle(child);
317
+ expect(runtime.worldBoundsRectangle).not.toEqual(cache);
318
+ expect(equalsRectangle(runtime.worldBoundsRectangle, { x: 100, y: 0, width: 0, height: 0 })).toBe(true);
319
+ });
320
+
321
+ it('should recalculate if parent transform is changed (scale)', () => {
322
+ const parent = createTestNode();
323
+ const child = createTestNode();
324
+ addNodeChild(parent, child);
325
+ const runtime = getEntityRuntime(child);
326
+ ensureNodeWorldBoundsRectangle(child);
327
+ // write the cached rectangles directly (bypassing invalidation) so the scaled recalculation is observable
328
+ const localBounds = getNodeLocalBoundsRectangle(child) as Rectangle;
329
+ localBounds.width = 10;
330
+ const worldBounds = getNodeWorldBoundsRectangle(child) as Rectangle;
331
+ worldBounds.width = 10;
332
+ const cache = cloneRectangle(runtime.worldBoundsRectangle!);
333
+ parent.scaleX = 2;
334
+ invalidateNodeLocalTransform(parent);
335
+ ensureNodeLocalTransformMatrix(parent);
336
+ ensureNodeWorldBoundsRectangle(child);
337
+ expect(runtime.worldBoundsRectangle).not.toEqual(cache);
338
+ expect(equalsRectangle(runtime.worldBoundsRectangle, { x: 0, y: 0, width: 20, height: 0 })).toBe(true);
339
+ });
340
+ });
341
+
342
+ describe('getNodeHeight', () => {
343
+ it('returns height in parent space with default scale', () => {
344
+ const parent = createTestNode();
345
+ const node = createTestNode();
346
+ addNodeChild(parent, node);
347
+ setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 100, 50);
348
+ expect(getNodeHeight(node)).toBeCloseTo(50);
349
+ });
350
+
351
+ it('accounts for scaleY', () => {
352
+ const parent = createTestNode();
353
+ const node = createTestNode();
354
+ addNodeChild(parent, node);
355
+ setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 100, 50);
356
+ node.scaleY = 2;
357
+ invalidateNodeLocalTransform(node);
358
+ expect(getNodeHeight(node)).toBeCloseTo(100);
359
+ });
360
+ });
361
+
362
+ describe('getNodeLocalBoundsRectangle', () => {
363
+ it('should call ensure and return localBoundsRectangle', () => {
364
+ const object = createTestNode();
365
+ const runtime = getEntityRuntime(object);
366
+ expect(runtime.localBoundsRectangle).toBeNull();
367
+ const rect = getNodeLocalBoundsRectangle(object);
368
+ expect(rect).not.toBeNull();
369
+ expect(rect).toStrictEqual(runtime.localBoundsRectangle);
370
+ });
371
+ });
372
+
373
+ describe('getNodeParentBoundsRectangle', () => {
374
+ it('should call ensure and return boundsRectangle', () => {
375
+ const object = createTestNode();
376
+ const runtime = getEntityRuntime(object);
377
+ expect(runtime.boundsRectangle).toBeNull();
378
+ const rect = getNodeParentBoundsRectangle(object);
379
+ expect(rect).not.toBeNull();
380
+ expect(rect).toStrictEqual(runtime.boundsRectangle);
381
+ });
382
+ });
383
+
384
+ describe('getNodeWidth', () => {
385
+ it('returns width in parent space with default scale', () => {
386
+ const parent = createTestNode();
387
+ const node = createTestNode();
388
+ addNodeChild(parent, node);
389
+ setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 80, 40);
390
+ expect(getNodeWidth(node)).toBeCloseTo(80);
391
+ });
392
+
393
+ it('accounts for scaleX', () => {
394
+ const parent = createTestNode();
395
+ const node = createTestNode();
396
+ addNodeChild(parent, node);
397
+ setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 80, 40);
398
+ node.scaleX = 3;
399
+ invalidateNodeLocalTransform(node);
400
+ expect(getNodeWidth(node)).toBeCloseTo(240);
401
+ });
402
+ });
403
+
404
+ describe('getNodeWorldBoundsRectangle', () => {
405
+ it('should call ensure and return worldBoundsRectangle', () => {
406
+ const object = createTestNode();
407
+ const runtime = getEntityRuntime(object);
408
+ expect(runtime.worldBoundsRectangle).toBeNull();
409
+ const rect = getNodeWorldBoundsRectangle(object);
410
+ expect(rect).not.toBeNull();
411
+ expect(rect).toStrictEqual(runtime.worldBoundsRectangle);
412
+ });
413
+
414
+ it('excludes a disabled child from world bounds', () => {
415
+ const parent = createTestNode();
416
+ const child = createTestNode();
417
+ addNodeChild(parent, child);
418
+
419
+ setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
420
+ setRectangle(getNodeLocalBoundsRectangle(child) as Rectangle, 0, 0, 200, 200);
421
+ child.x = 100;
422
+ invalidateNodeLocalTransform(child);
423
+
424
+ child.enabled = false;
425
+
426
+ const bounds = getNodeWorldBoundsRectangle(parent);
427
+ expect(bounds.width).toBeCloseTo(10);
428
+ expect(bounds.height).toBeCloseTo(10);
429
+ });
430
+
431
+ it('includes an enabled child in world bounds', () => {
432
+ const parent = createTestNode();
433
+ const child = createTestNode();
434
+ addNodeChild(parent, child);
435
+
436
+ setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
437
+ setRectangle(getNodeLocalBoundsRectangle(child) as Rectangle, 0, 0, 200, 200);
438
+ child.x = 100;
439
+ invalidateNodeLocalTransform(child);
440
+
441
+ const bounds = getNodeWorldBoundsRectangle(parent);
442
+ expect(bounds.width).toBeGreaterThan(10);
443
+ expect(bounds.height).toBeGreaterThan(10);
444
+ });
445
+ });
446
+
447
+ describe('setNodeHeight', () => {
448
+ it('adjusts scaleY to achieve the desired height', () => {
449
+ const parent = createTestNode();
450
+ const node = createTestNode();
451
+ addNodeChild(parent, node);
452
+ setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 100, 50);
453
+ setNodeHeight(node, 100);
454
+ expect(getNodeHeight(node)).toBeCloseTo(100);
455
+ });
456
+
457
+ it('does nothing when scaleY is zero', () => {
458
+ const node = createTestNode();
459
+ node.scaleY = 0;
460
+ invalidateNodeLocalTransform(node);
461
+ setNodeHeight(node, 100);
462
+ expect(node.scaleY).toBe(0);
463
+ });
464
+ });
465
+
466
+ describe('setNodeWidth', () => {
467
+ it('adjusts scaleX to achieve the desired width', () => {
468
+ const parent = createTestNode();
469
+ const node = createTestNode();
470
+ addNodeChild(parent, node);
471
+ setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 80, 40);
472
+ setNodeWidth(node, 160);
473
+ expect(getNodeWidth(node)).toBeCloseTo(160);
474
+ });
475
+
476
+ it('does nothing when scaleX is zero', () => {
477
+ const node = createTestNode();
478
+ node.scaleX = 0;
479
+ invalidateNodeLocalTransform(node);
480
+ setNodeWidth(node, 100);
481
+ expect(node.scaleX).toBe(0);
482
+ });
483
+ });
484
+
485
+ function cloneAndInvalidateRect(rect: Rectangle): Rectangle {
486
+ const clone = cloneRectangle(rect);
487
+ invalidateRect(rect);
488
+ return clone;
489
+ }
490
+
491
+ function invalidateRect(rect: Rectangle | null): void {
492
+ if (rect !== null) setRectangle(rect, NaN, NaN, NaN, NaN);
493
+ }
494
+
495
+ type TestNode = Node<HasTransform2D & HasBoundsRectangle> & HasTransform2D & HasBoundsRectangle;
496
+
497
+ const TestKind = 'Test';
@@ -0,0 +1,47 @@
1
+ import type { HasAppearance } from '@flighthq/types';
2
+ import { BlendMode } from '@flighthq/types';
3
+
4
+ import { initAppearanceTrait } from './hasAppearance';
5
+ import { createNode } from './node';
6
+
7
+ const TestKind = 'Test';
8
+
9
+ function makeTarget(): HasAppearance {
10
+ const node = createNode(TestKind) as unknown as HasAppearance;
11
+ return node;
12
+ }
13
+
14
+ describe('initAppearanceTrait', () => {
15
+ it('sets default values when called with no options', () => {
16
+ const target = makeTarget();
17
+ initAppearanceTrait(target);
18
+
19
+ expect(target.alpha).toBe(1);
20
+ expect(target.blendMode).toBeNull();
21
+ expect(target.visible).toBe(true);
22
+ });
23
+
24
+ it('applies partial overrides', () => {
25
+ const target = makeTarget();
26
+ initAppearanceTrait(target, { alpha: 0.5, visible: false });
27
+
28
+ expect(target.alpha).toBe(0.5);
29
+ expect(target.visible).toBe(false);
30
+ expect(target.blendMode).toBeNull();
31
+ });
32
+
33
+ it('applies blendMode override', () => {
34
+ const target = makeTarget();
35
+ initAppearanceTrait(target, { blendMode: BlendMode.Add });
36
+
37
+ expect(target.blendMode).toBe(BlendMode.Add);
38
+ });
39
+
40
+ it('overwrites existing values', () => {
41
+ const target = makeTarget();
42
+ initAppearanceTrait(target);
43
+ initAppearanceTrait(target, { alpha: 0.25 });
44
+
45
+ expect(target.alpha).toBe(0.25);
46
+ });
47
+ });
@@ -0,0 +1,57 @@
1
+ import { createRectangle } from '@flighthq/geometry';
2
+ import type { HasBoundsRectangle, HasBoundsRectangleRuntime, Node, NodeRuntime } from '@flighthq/types';
3
+
4
+ import {
5
+ defaultComputeLocalBoundsRectangle,
6
+ initBoundsRectangleRuntimeTrait,
7
+ initBoundsRectangleTrait,
8
+ } from './hasBoundsRectangle';
9
+ import { createNode, createNodeRuntime } from './node';
10
+
11
+ describe('defaultComputeLocalBoundsRectangle', () => {
12
+ it('is a no-op that does not modify out', () => {
13
+ const out = createRectangle(1, 2, 3, 4);
14
+ const node = createNode(NodeTestKind);
15
+ defaultComputeLocalBoundsRectangle(out, node as unknown as Node);
16
+ expect(out.x).toBe(1);
17
+ expect(out.y).toBe(2);
18
+ expect(out.width).toBe(3);
19
+ expect(out.height).toBe(4);
20
+ });
21
+ });
22
+
23
+ describe('initBoundsRectangleRuntimeTrait', () => {
24
+ let runtime: HasBoundsRectangleRuntime;
25
+
26
+ beforeEach(() => {
27
+ runtime = createNodeRuntime() as NodeRuntime<HasBoundsRectangle> & HasBoundsRectangleRuntime;
28
+ });
29
+
30
+ it('initializes default values', () => {
31
+ initBoundsRectangleRuntimeTrait(runtime);
32
+
33
+ expect(runtime.boundsRectangle).toBeNull();
34
+ expect(runtime.localBoundsRectangle).toBeNull();
35
+ expect(runtime.worldBoundsRectangle).toBeNull();
36
+ expect(runtime.computeLocalBoundsRectangle).toStrictEqual(defaultComputeLocalBoundsRectangle);
37
+ });
38
+ });
39
+
40
+ describe('initBoundsRectangleTrait', () => {
41
+ let node: HasBoundsRectangle;
42
+
43
+ beforeEach(() => {
44
+ node = createNode(NodeTestKind) as Node<HasBoundsRectangle> & HasBoundsRectangle;
45
+ });
46
+
47
+ it('does nothing', () => {
48
+ initBoundsRectangleTrait(node);
49
+ });
50
+
51
+ it('allows pre-defined values', () => {
52
+ const base = {};
53
+ initBoundsRectangleTrait(node, base);
54
+ });
55
+ });
56
+
57
+ const NodeTestKind = 'NodeTest';
@@ -0,0 +1,27 @@
1
+ import type { ClipRegion, HasClip } from '@flighthq/types';
2
+
3
+ import { initClipTrait } from './hasClip';
4
+ import { createNode } from './node';
5
+
6
+ const TestKind = 'Test';
7
+
8
+ function makeTarget(): HasClip {
9
+ return createNode(TestKind) as unknown as HasClip;
10
+ }
11
+
12
+ describe('initClipTrait', () => {
13
+ it('defaults clip to null', () => {
14
+ const target = makeTarget();
15
+ initClipTrait(target);
16
+
17
+ expect(target.clip).toBeNull();
18
+ });
19
+
20
+ it('applies a clip override', () => {
21
+ const clip = { contours: null, rect: { height: 4, width: 3, x: 1, y: 2 }, version: 0 } as ClipRegion;
22
+ const target = makeTarget();
23
+ initClipTrait(target, { clip });
24
+
25
+ expect(target.clip).toBe(clip);
26
+ });
27
+ });
@@ -0,0 +1,30 @@
1
+ import type { HasMaterial } from '@flighthq/types';
2
+
3
+ import { initMaterialTrait } from './hasMaterial';
4
+ import { createNode } from './node';
5
+
6
+ const TestKind = 'Test';
7
+
8
+ function makeTarget(): HasMaterial {
9
+ return createNode(TestKind) as unknown as HasMaterial;
10
+ }
11
+
12
+ describe('initMaterialTrait', () => {
13
+ it('defaults material and materialData to null', () => {
14
+ const target = makeTarget();
15
+ initMaterialTrait(target);
16
+
17
+ expect(target.material).toBeNull();
18
+ expect(target.materialData).toBeNull();
19
+ });
20
+
21
+ it('applies material and materialData overrides', () => {
22
+ const material = { kind: 'Mat' } as any;
23
+ const materialData = {} as any;
24
+ const target = makeTarget();
25
+ initMaterialTrait(target, { material, materialData });
26
+
27
+ expect(target.material).toBe(material);
28
+ expect(target.materialData).toBe(materialData);
29
+ });
30
+ });