@flighthq/node 0.3.0-next.906.07cea63 → 0.3.1-next.1041.35b950c

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/boundsRectangle.d.ts +11 -0
  2. package/dist/boundsRectangle.d.ts.map +1 -1
  3. package/dist/boundsRectangle.js +110 -12
  4. package/dist/boundsRectangle.js.map +1 -1
  5. package/dist/contract.d.ts +2 -0
  6. package/dist/contract.d.ts.map +1 -1
  7. package/dist/contract.js +2 -0
  8. package/dist/contract.js.map +1 -1
  9. package/dist/enableNodeGuards.d.ts +4 -0
  10. package/dist/enableNodeGuards.d.ts.map +1 -0
  11. package/dist/enableNodeGuards.js +29 -0
  12. package/dist/enableNodeGuards.js.map +1 -0
  13. package/dist/hasAppearance.d.ts +2 -1
  14. package/dist/hasAppearance.d.ts.map +1 -1
  15. package/dist/hasAppearance.js +10 -0
  16. package/dist/hasAppearance.js.map +1 -1
  17. package/dist/hasBoundsRectangle.d.ts +1 -1
  18. package/dist/hasBoundsRectangle.d.ts.map +1 -1
  19. package/dist/hasBoundsRectangle.js +5 -0
  20. package/dist/hasBoundsRectangle.js.map +1 -1
  21. package/dist/hasTransform3d.d.ts.map +1 -1
  22. package/dist/hasTransform3d.js +11 -4
  23. package/dist/hasTransform3d.js.map +1 -1
  24. package/dist/hierarchy.d.ts +15 -7
  25. package/dist/hierarchy.d.ts.map +1 -1
  26. package/dist/hierarchy.js +65 -28
  27. package/dist/hierarchy.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/node.d.ts.map +1 -1
  33. package/dist/node.js +2 -0
  34. package/dist/node.js.map +1 -1
  35. package/dist/nodeColorAdjustment.d.ts.map +1 -1
  36. package/dist/nodeColorAdjustment.js +1 -7
  37. package/dist/nodeColorAdjustment.js.map +1 -1
  38. package/dist/nodeOrderList.d.ts +88 -0
  39. package/dist/nodeOrderList.d.ts.map +1 -0
  40. package/dist/nodeOrderList.js +257 -0
  41. package/dist/nodeOrderList.js.map +1 -0
  42. package/dist/nodeTransform2d.d.ts.map +1 -1
  43. package/dist/nodeTransform2d.js +1 -1
  44. package/dist/nodeTransform2d.js.map +1 -1
  45. package/dist/revision.d.ts +5 -2
  46. package/dist/revision.d.ts.map +1 -1
  47. package/dist/revision.js +17 -5
  48. package/dist/revision.js.map +1 -1
  49. package/dist/stageFit.d.ts.map +1 -1
  50. package/dist/stageFit.js +8 -4
  51. package/dist/stageFit.js.map +1 -1
  52. package/dist/traversal.d.ts +1 -1
  53. package/dist/traversal.d.ts.map +1 -1
  54. package/dist/traversal.js +1 -1
  55. package/dist/traversal.js.map +1 -1
  56. package/package.json +11 -7
  57. package/src/boundsRectangle.test.ts +378 -7
  58. package/src/enableNodeGuards.test.ts +78 -0
  59. package/src/hasAppearance.test.ts +33 -3
  60. package/src/hasBoundsRectangle.test.ts +8 -0
  61. package/src/hasTransform2d.test.ts +6 -0
  62. package/src/hasTransform3d.test.ts +40 -8
  63. package/src/hierarchy.test.ts +298 -4
  64. package/src/node.test.ts +11 -1
  65. package/src/nodeColorAdjustment.test.ts +11 -0
  66. package/src/nodeOrderList.test.ts +683 -0
  67. package/src/nodeTransform2d.test.ts +45 -0
  68. package/src/revision.test.ts +51 -0
  69. package/src/stageFit.test.ts +56 -1
  70. package/src/traversal.test.ts +32 -3
@@ -166,6 +166,28 @@ describe('ensureNodeLocalMatrix', () => {
166
166
  ensureNodeLocalMatrix(node);
167
167
  expect(equalsMatrix(runtime.localMatrix, cache)).toBe(true);
168
168
  });
169
+
170
+ it('normalizes rotations above 180 degrees', () => {
171
+ const runtime = getEntityRuntime(node) as HasTransform2DRuntime;
172
+ node.rotation = 270;
173
+ invalidateNodeLocalTransform(node);
174
+
175
+ ensureNodeLocalMatrix(node);
176
+
177
+ expect(runtime.rotationAngle).toBe(-90);
178
+ expect(runtime.localMatrix?.b).toBeCloseTo(-1);
179
+ });
180
+
181
+ it('normalizes rotations below -180 degrees', () => {
182
+ const runtime = getEntityRuntime(node) as HasTransform2DRuntime;
183
+ node.rotation = -270;
184
+ invalidateNodeLocalTransform(node);
185
+
186
+ ensureNodeLocalMatrix(node);
187
+
188
+ expect(runtime.rotationAngle).toBe(90);
189
+ expect(runtime.localMatrix?.b).toBeCloseTo(1);
190
+ });
169
191
  });
170
192
 
171
193
  describe('ensureNodeWorldMatrix', () => {
@@ -282,6 +304,29 @@ describe('setNodeLocalMatrix', () => {
282
304
  expect(m.ty).toBeCloseTo(20);
283
305
  });
284
306
 
307
+ // Pins the losslessness this function's contract promises for a reflected (negative determinant)
308
+ // matrix. Positive-determinant cases round-tripped even while reflected ones silently lost the
309
+ // reflection, so a fixture that is not reflected cannot speak to this.
310
+ it('round-trips a reflected matrix, keeping the mirror', () => {
311
+ setNodeLocalMatrix(node, createMatrix(1, 0, 0, -1, 0, 0));
312
+ const m = getNodeLocalMatrix(node);
313
+ expect(m.a).toBeCloseTo(1);
314
+ expect(m.b).toBeCloseTo(0);
315
+ expect(m.c).toBeCloseTo(0);
316
+ expect(m.d).toBeCloseTo(-1);
317
+ });
318
+
319
+ it('round-trips a rotated reflection through all four matrix cells', () => {
320
+ setNodeLocalMatrix(node, createMatrix(0.5, 0.8, 0.8, -0.5, 3, 4));
321
+ const m = getNodeLocalMatrix(node);
322
+ expect(m.a).toBeCloseTo(0.5);
323
+ expect(m.b).toBeCloseTo(0.8);
324
+ expect(m.c).toBeCloseTo(0.8);
325
+ expect(m.d).toBeCloseTo(-0.5);
326
+ expect(m.tx).toBeCloseTo(3);
327
+ expect(m.ty).toBeCloseTo(4);
328
+ });
329
+
285
330
  it('resets pivot and absorbs its offset into translation', () => {
286
331
  node.pivotX = 5;
287
332
  node.pivotY = 5;
@@ -1,12 +1,15 @@
1
1
  import { createNode, getNodeRuntime } from '@flighthq/node/contract';
2
2
  import type { Node, NodeRuntime } from '@flighthq/types/contract';
3
3
 
4
+ import { addNodeChild } from './hierarchy';
4
5
  import {
5
6
  computeNodeWorldTransformRevision,
6
7
  getNodeAppearanceRevision,
8
+ getNodeChildrenRevision,
7
9
  getNodeLocalBoundsRevision,
8
10
  getNodeLocalContentRevision,
9
11
  getNodeLocalTransformRevision,
12
+ getNodeParentReferenceRevision,
10
13
  getNodeWorldTransformRevision,
11
14
  invalidateContent,
12
15
  invalidateNode,
@@ -39,6 +42,7 @@ describe('computeNodeWorldTransformRevision', () => {
39
42
  computeNodeWorldTransformRevision(runtime);
40
43
  expect(runtime.worldTransformUsingLocalTransformId).toBe(3);
41
44
  expect(runtime.worldTransformUsingParentTransformId).toBe(0);
45
+ expect(runtime.worldTransformId).not.toBe(0);
42
46
  });
43
47
 
44
48
  it('incorporates parent worldTransformId when provided', () => {
@@ -49,6 +53,17 @@ describe('computeNodeWorldTransformRevision', () => {
49
53
  computeNodeWorldTransformRevision(runtime, parentRuntime);
50
54
  expect(runtime.worldTransformUsingParentTransformId).toBe(7);
51
55
  });
56
+
57
+ it('assigns the next monotonic world revision on every recompute', () => {
58
+ const runtime = getEntityRuntime(node);
59
+ computeNodeWorldTransformRevision(runtime);
60
+ const first = runtime.worldTransformId;
61
+
62
+ computeNodeWorldTransformRevision(runtime);
63
+
64
+ const next = (first + 1) >>> 0;
65
+ expect(runtime.worldTransformId).toBe(next === 0 ? 1 : next);
66
+ });
52
67
  });
53
68
 
54
69
  describe('getNodeAppearanceRevision', () => {
@@ -59,6 +74,14 @@ describe('getNodeAppearanceRevision', () => {
59
74
  });
60
75
  });
61
76
 
77
+ describe('getNodeChildrenRevision', () => {
78
+ it('returns childrenId', () => {
79
+ const runtime = getEntityRuntime(node);
80
+ runtime.childrenId = 100;
81
+ expect(getNodeChildrenRevision(node)).toStrictEqual(runtime.childrenId);
82
+ });
83
+ });
84
+
62
85
  describe('getNodeLocalBoundsRevision', () => {
63
86
  it('returns localBoundsId', () => {
64
87
  const runtime = getEntityRuntime(node);
@@ -83,6 +106,14 @@ describe('getNodeLocalTransformRevision', () => {
83
106
  });
84
107
  });
85
108
 
109
+ describe('getNodeParentReferenceRevision', () => {
110
+ it('returns parentReferenceId', () => {
111
+ const runtime = getEntityRuntime(node);
112
+ runtime.parentReferenceId = 100;
113
+ expect(getNodeParentReferenceRevision(node)).toStrictEqual(runtime.parentReferenceId);
114
+ });
115
+ });
116
+
86
117
  describe('getNodeWorldTransformRevision', () => {
87
118
  it('returns worldTransformId', () => {
88
119
  const runtime = getEntityRuntime(node);
@@ -193,6 +224,13 @@ describe('invalidateNodeLocalTransform', () => {
193
224
  });
194
225
 
195
226
  describe('invalidateNodeParentReference', () => {
227
+ it('increments parentReferenceId', () => {
228
+ const runtime = getEntityRuntime(node);
229
+ const parentReferenceId = runtime.parentReferenceId;
230
+ invalidateNodeParentReference(node);
231
+ expect(runtime.parentReferenceId).toBe(parentReferenceId + 1);
232
+ });
233
+
196
234
  it('invalidates the world transform parent transform cached ID', () => {
197
235
  const runtime = getEntityRuntime(node);
198
236
  runtime.worldTransformUsingParentTransformId = 1;
@@ -221,6 +259,19 @@ describe('invalidateNodeWorldBounds', () => {
221
259
  expect(runtime.worldBoundsUsingWorldTransformId).toBe(-1);
222
260
  expect(runtime.worldBoundsUsingLocalBoundsId).toBe(-1);
223
261
  });
262
+
263
+ it('invalidates every ancestor whose aggregate includes the target', () => {
264
+ const parent = createTestNode();
265
+ addNodeChild(parent, node);
266
+ const parentRuntime = getEntityRuntime(parent);
267
+ parentRuntime.worldBoundsUsingWorldTransformId = 1;
268
+ parentRuntime.worldBoundsUsingLocalBoundsId = 1;
269
+
270
+ invalidateNodeWorldBounds(node);
271
+
272
+ expect(parentRuntime.worldBoundsUsingWorldTransformId).toBe(-1);
273
+ expect(parentRuntime.worldBoundsUsingLocalBoundsId).toBe(-1);
274
+ });
224
275
  });
225
276
 
226
277
  type TestNode = Node;
@@ -18,10 +18,12 @@ import {
18
18
 
19
19
  const TestNodeKind = 'TestNode';
20
20
 
21
- function makeNodeWithBounds(width: number, height: number) {
21
+ function makeNodeWithBounds(width: number, height: number, x = 0, y = 0) {
22
22
  const node = createNode(TestNodeKind);
23
23
  const runtime = getNodeRuntime(node) as unknown as HasBoundsRectangleRuntime;
24
24
  runtime.computeLocalBoundsRectangle = (out) => {
25
+ out.x = x;
26
+ out.y = y;
25
27
  out.width = width;
26
28
  out.height = height;
27
29
  };
@@ -137,6 +139,18 @@ describe('computeScene2DFitTransform', () => {
137
139
  expect(m.d).toBe(1);
138
140
  });
139
141
 
142
+ it('sets identity when only the content width is zero', () => {
143
+ const m = createMatrix(2, 3, 4, 5, 6, 7);
144
+ computeScene2DFitTransform(m, fit({ root: makeNodeWithBounds(0, 50), scaleMode: 'exactfit' }), 800, 600);
145
+ expect(m).toMatchObject({ a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 });
146
+ });
147
+
148
+ it('sets identity when only the content height is zero', () => {
149
+ const m = createMatrix(2, 3, 4, 5, 6, 7);
150
+ computeScene2DFitTransform(m, fit({ root: makeNodeWithBounds(100, 0), scaleMode: 'exactfit' }), 800, 600);
151
+ expect(m).toMatchObject({ a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 });
152
+ });
153
+
140
154
  it('noscale with topleft: identity scale at origin', () => {
141
155
  const m = createMatrix();
142
156
  computeScene2DFitTransform(
@@ -226,4 +240,45 @@ describe('computeScene2DFitTransform', () => {
226
240
  expect(m.b).toBe(0);
227
241
  expect(m.c).toBe(0);
228
242
  });
243
+
244
+ it('maps a nonzero content-bounds origin to the selected alignment', () => {
245
+ const m = createMatrix();
246
+ computeScene2DFitTransform(
247
+ m,
248
+ fit({ align: 'topleft', root: makeNodeWithBounds(100, 50, 10, -20), scaleMode: 'exactfit' }),
249
+ 200,
250
+ 100,
251
+ );
252
+
253
+ expect(m.a).toBe(2);
254
+ expect(m.d).toBe(2);
255
+ expect(m.tx).toBe(-20);
256
+ expect(m.ty).toBe(40);
257
+ });
258
+
259
+ it('uses scaled width before subtracting a nonzero origin on a centered horizontal axis', () => {
260
+ const m = createMatrix();
261
+ computeScene2DFitTransform(
262
+ m,
263
+ fit({ align: 'top', root: makeNodeWithBounds(100, 50, 10, -20), scaleMode: 'exactfit' }),
264
+ 200,
265
+ 100,
266
+ );
267
+
268
+ expect(m.tx).toBe(-20);
269
+ expect(m.ty).toBe(40);
270
+ });
271
+
272
+ it('uses scaled height before subtracting a nonzero origin on a centered vertical axis', () => {
273
+ const m = createMatrix();
274
+ computeScene2DFitTransform(
275
+ m,
276
+ fit({ align: 'left', root: makeNodeWithBounds(100, 50, 10, -20), scaleMode: 'exactfit' }),
277
+ 200,
278
+ 100,
279
+ );
280
+
281
+ expect(m.tx).toBe(-20);
282
+ expect(m.ty).toBe(40);
283
+ });
229
284
  });
@@ -1,8 +1,8 @@
1
- import type { Node, NodeTraits } from '@flighthq/types/contract';
1
+ import type { Node, NodeRuntime, NodeTraits } from '@flighthq/types/contract';
2
2
  import { NodeKind } from '@flighthq/types/contract';
3
3
 
4
4
  import { addNodeChild } from './hierarchy';
5
- import { createNode } from './node';
5
+ import { createNode, getNodeRuntime } from './node';
6
6
  import {
7
7
  findNode,
8
8
  findNodeByName,
@@ -112,7 +112,6 @@ describe('forEachNodeAncestor', () => {
112
112
  const visited: Node<NodeTraits>[] = [];
113
113
  forEachNodeAncestor(grandchild, (n) => {
114
114
  visited.push(n);
115
- return true;
116
115
  });
117
116
  expect(visited).toEqual([childA, root]);
118
117
  });
@@ -191,6 +190,18 @@ describe('getNodeNextSibling', () => {
191
190
  it('returns the next sibling', () => {
192
191
  expect(getNodeNextSibling(childA)).toBe(childB);
193
192
  });
193
+
194
+ it('returns null for a stale parent whose child list is absent', () => {
195
+ (getNodeRuntime(root) as NodeRuntime).children = null;
196
+
197
+ expect(getNodeNextSibling(childA)).toBeNull();
198
+ });
199
+
200
+ it('returns null for a stale parent whose nonempty child list omits the source', () => {
201
+ (getNodeRuntime(root) as NodeRuntime).children = [childB];
202
+
203
+ expect(getNodeNextSibling(childA)).toBeNull();
204
+ });
194
205
  });
195
206
 
196
207
  describe('getNodePreviousSibling', () => {
@@ -205,6 +216,12 @@ describe('getNodePreviousSibling', () => {
205
216
  it('returns the previous sibling', () => {
206
217
  expect(getNodePreviousSibling(childB)).toBe(childA);
207
218
  });
219
+
220
+ it('returns null for a stale parent whose child list is absent', () => {
221
+ (getNodeRuntime(root) as NodeRuntime).children = null;
222
+
223
+ expect(getNodePreviousSibling(childB)).toBeNull();
224
+ });
208
225
  });
209
226
 
210
227
  describe('walkNodeDescendants', () => {
@@ -243,4 +260,16 @@ describe('walkNodeDescendants', () => {
243
260
  expect(visited).not.toContain(grandchild);
244
261
  expect(visited).not.toContain(childB);
245
262
  });
263
+
264
+ it('propagates an early stop from a nested descendant', () => {
265
+ const visited: Node<NodeTraits>[] = [];
266
+
267
+ const result = walkNodeDescendants(root, (node) => {
268
+ visited.push(node);
269
+ return node !== grandchild;
270
+ });
271
+
272
+ expect(result).toBe(false);
273
+ expect(visited).toEqual([childA, grandchild]);
274
+ });
246
275
  });