@flighthq/node 0.3.1-next.625.675880d → 0.3.1-next.704.69286f6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"nodeColorAdjustment.d.ts","sourceRoot":"","sources":["../src/nodeColorAdjustment.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAA0B,IAAI,EAAe,MAAM,0BAA0B,CAAC;AAKtG;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAMhH;AAED,wFAAwF;AACxF,wBAAgB,uBAAuB,CAAC,MAAM,SAAS,MAAM,EAC3D,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAC7B,SAAS,UAAU,EAAE,GAAG,IAAI,CAE9B;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAS,MAAM,EAC3D,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EACpB,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,GAClC,IAAI,CAKN;AAED,wEAAwE;AACxE,wBAAgB,2BAA2B,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE3G"}
1
+ {"version":3,"file":"nodeColorAdjustment.d.ts","sourceRoot":"","sources":["../src/nodeColorAdjustment.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAA0B,IAAI,EAAe,MAAM,0BAA0B,CAAC;AAKtG;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAMhH;AAED,wFAAwF;AACxF,wBAAgB,uBAAuB,CAAC,MAAM,SAAS,MAAM,EAC3D,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAC7B,SAAS,UAAU,EAAE,GAAG,IAAI,CAE9B;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAS,MAAM,EAC3D,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EACpB,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,GAClC,IAAI,CAKN;AAED,wEAAwE;AACxE,wBAAgB,2BAA2B,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE3G"}
@@ -1,4 +1,4 @@
1
- import { COLOR_ADJUSTMENT_CHANNEL_MIXING, COLOR_ADJUSTMENT_NONE, createTintAdjustment, resolveColorAdjustmentsColorMatrix, resolveColorAdjustmentsColorScaleBias, } from '@flighthq/adjustments/contract';
1
+ import { COLOR_ADJUSTMENT_CHANNEL_MIXING, createTintAdjustment, resolveColorAdjustmentsColorMatrix, resolveColorAdjustmentsColorScaleBias, } from '@flighthq/adjustments/contract';
2
2
  import { createColorScaleBias } from '@flighthq/materials/contract';
3
3
  import { getNodeRuntime } from './node';
4
4
  import { invalidateNodeAppearance } from './revision';
@@ -45,12 +45,6 @@ function resolveNodeColorAdjustments(runtime) {
45
45
  }
46
46
  const out = runtime.resolvedColorScaleBias ?? createColorScaleBias();
47
47
  const status = resolveColorAdjustmentsColorScaleBias(adjustments, out);
48
- if (status === COLOR_ADJUSTMENT_NONE) {
49
- runtime.resolvedColorScaleBias = null;
50
- runtime.resolvedColorMatrix = null;
51
- runtime.colorAdjustmentsUnsupported = false;
52
- return;
53
- }
54
48
  runtime.resolvedColorScaleBias = out;
55
49
  runtime.resolvedColorMatrix =
56
50
  status === COLOR_ADJUSTMENT_CHANNEL_MIXING ? resolveColorAdjustmentsColorMatrix(adjustments) : null;
@@ -1 +1 @@
1
- {"version":3,"file":"nodeColorAdjustment.js","sourceRoot":"","sources":["../src/nodeColorAdjustment.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,qBAAqB,EACrB,oBAAoB,EACpB,kCAAkC,EAClC,qCAAqC,GACtC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAwB,MAAoB,EAAE,UAAsB;IACxG,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAwB,CAAC;IAC9D,MAAM,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IACzC,OAAO,CAAC,gBAAgB,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,CAAC,CAAC;IACtF,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACrC,wBAAwB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,uBAAuB,CACrC,MAA8B;IAE9B,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAoB,EACpB,KAAmC;IAEnC,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAwB,CAAC;IAC9D,OAAO,CAAC,gBAAgB,GAAG,KAAK,CAAC;IACjC,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACrC,wBAAwB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,2BAA2B,CAAwB,MAAoB,EAAE,IAAY;IACnG,uBAAuB,CAAC,MAAM,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,2BAA2B,CAAC,OAA+B;IAClE,MAAM,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAC7C,IAAI,WAAW,KAAK,IAAI,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;QACtC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;QACnC,OAAO,CAAC,2BAA2B,GAAG,KAAK,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,sBAAsB,IAAI,oBAAoB,EAAE,CAAC;IACrE,MAAM,MAAM,GAAG,qCAAqC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IACvE,IAAI,MAAM,KAAK,qBAAqB,EAAE,CAAC;QACrC,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;QACtC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;QACnC,OAAO,CAAC,2BAA2B,GAAG,KAAK,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,OAAO,CAAC,sBAAsB,GAAG,GAAG,CAAC;IACrC,OAAO,CAAC,mBAAmB;QACzB,MAAM,KAAK,+BAA+B,CAAC,CAAC,CAAC,kCAAkC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtG,OAAO,CAAC,2BAA2B;QACjC,MAAM,KAAK,+BAA+B,IAAI,OAAO,CAAC,mBAAmB,KAAK,IAAI,CAAC;AACvF,CAAC"}
1
+ {"version":3,"file":"nodeColorAdjustment.js","sourceRoot":"","sources":["../src/nodeColorAdjustment.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,oBAAoB,EACpB,kCAAkC,EAClC,qCAAqC,GACtC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAwB,MAAoB,EAAE,UAAsB;IACxG,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAwB,CAAC;IAC9D,MAAM,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IACzC,OAAO,CAAC,gBAAgB,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,CAAC,CAAC;IACtF,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACrC,wBAAwB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,uBAAuB,CACrC,MAA8B;IAE9B,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAoB,EACpB,KAAmC;IAEnC,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAwB,CAAC;IAC9D,OAAO,CAAC,gBAAgB,GAAG,KAAK,CAAC;IACjC,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACrC,wBAAwB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,2BAA2B,CAAwB,MAAoB,EAAE,IAAY;IACnG,uBAAuB,CAAC,MAAM,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,2BAA2B,CAAC,OAA+B;IAClE,MAAM,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAC7C,IAAI,WAAW,KAAK,IAAI,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;QACtC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;QACnC,OAAO,CAAC,2BAA2B,GAAG,KAAK,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,sBAAsB,IAAI,oBAAoB,EAAE,CAAC;IACrE,MAAM,MAAM,GAAG,qCAAqC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IACvE,OAAO,CAAC,sBAAsB,GAAG,GAAG,CAAC;IACrC,OAAO,CAAC,mBAAmB;QACzB,MAAM,KAAK,+BAA+B,CAAC,CAAC,CAAC,kCAAkC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtG,OAAO,CAAC,2BAA2B;QACjC,MAAM,KAAK,+BAA+B,IAAI,OAAO,CAAC,mBAAmB,KAAK,IAAI,CAAC;AACvF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/node",
3
- "version": "0.3.1-next.625.675880d",
3
+ "version": "0.3.1-next.704.69286f6",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,12 +38,12 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/adjustments": "0.3.1-next.625.675880d",
42
- "@flighthq/entity": "0.3.1-next.625.675880d",
43
- "@flighthq/geometry": "0.3.1-next.625.675880d",
44
- "@flighthq/materials": "0.3.1-next.625.675880d",
45
- "@flighthq/signals": "0.3.1-next.625.675880d",
46
- "@flighthq/types": "0.3.1-next.625.675880d"
41
+ "@flighthq/adjustments": "0.3.1-next.704.69286f6",
42
+ "@flighthq/entity": "0.3.1-next.704.69286f6",
43
+ "@flighthq/geometry": "0.3.1-next.704.69286f6",
44
+ "@flighthq/materials": "0.3.1-next.704.69286f6",
45
+ "@flighthq/signals": "0.3.1-next.704.69286f6",
46
+ "@flighthq/types": "0.3.1-next.704.69286f6"
47
47
  },
48
48
  "devDependencies": {
49
49
  "typescript": "^5.3.0"
@@ -42,8 +42,10 @@ import { initBoundsRectangleRuntimeTrait, initBoundsRectangleTrait } from './has
42
42
 
43
43
  function getEntityRuntime(
44
44
  source: TestNode,
45
- ): NodeRuntime<HasBoundsRectangle & HasTransform2D> & HasBoundsRectangleRuntime {
46
- return _getRuntime(source) as NodeRuntime<HasBoundsRectangle & HasTransform2D> & HasBoundsRectangleRuntime;
45
+ ): NodeRuntime<HasBoundsRectangle & HasTransform2D> & HasBoundsRectangleRuntime & HasTransform2DRuntime {
46
+ return _getRuntime(source) as NodeRuntime<HasBoundsRectangle & HasTransform2D> &
47
+ HasBoundsRectangleRuntime &
48
+ HasTransform2DRuntime;
47
49
  }
48
50
 
49
51
  function createTestNode(): TestNode {
@@ -90,6 +92,14 @@ describe('computeNodeBoundsRectangle', () => {
90
92
  expect(equalsRectangle(out, { x: 5, y: 5, width: 100, height: 100 })).toBe(true);
91
93
  });
92
94
 
95
+ it('defaults a null target coordinate space to the source', () => {
96
+ const out = createRectangle();
97
+
98
+ computeNodeBoundsRectangle(out, child, null);
99
+
100
+ expect(equalsRectangle(out, { x: 5, y: 5, width: 100, height: 100 })).toBe(true);
101
+ });
102
+
93
103
  it('should compute bounds relative to parent', () => {
94
104
  const out = createRectangle();
95
105
  computeNodeBoundsRectangle(out, child, root);
@@ -99,6 +109,14 @@ describe('computeNodeBoundsRectangle', () => {
99
109
  expect(out.height).toBeCloseTo(100);
100
110
  });
101
111
 
112
+ it('uses parent-space bounds directly when the parent is nested', () => {
113
+ const out = createRectangle();
114
+
115
+ computeNodeBoundsRectangle(out, grandChild, child);
116
+
117
+ expect(equalsRectangle(out, getNodeParentBoundsRectangle(grandChild))).toBe(true);
118
+ });
119
+
102
120
  it('should compute bounds relative to nested child', () => {
103
121
  const out = createRectangle();
104
122
  computeNodeBoundsRectangle(out, root, grandChild);
@@ -153,6 +171,17 @@ describe('computeNodeBoundsRectangle', () => {
153
171
  expect(equalsRectangle(out, getNodeWorldBoundsRectangle(child))).toBe(true);
154
172
  });
155
173
 
174
+ it('converts through world space for a nested unrelated target', () => {
175
+ const unrelatedRoot = createTestNode();
176
+ const unrelatedTarget = createTestNode();
177
+ addNodeChild(unrelatedRoot, unrelatedTarget);
178
+ const out = createRectangle();
179
+
180
+ computeNodeBoundsRectangle(out, grandChild, unrelatedTarget);
181
+
182
+ expect(equalsRectangle(out, getNodeWorldBoundsRectangle(grandChild))).toBe(true);
183
+ });
184
+
156
185
  it('should return world bounds if the target coordinate space is root', () => {
157
186
  const out = createRectangle();
158
187
  computeNodeBoundsRectangle(out, child, root);
@@ -197,6 +226,39 @@ describe('computeNodeRootLocalBoundsRectangle', () => {
197
226
  expect(out.width).toBeCloseTo(18);
198
227
  expect(out.height).toBeCloseTo(24);
199
228
  });
229
+
230
+ it('composes transforms at every descendant depth', () => {
231
+ const root = createTestNode();
232
+ const child = createTestNode();
233
+ const grandchild = createTestNode();
234
+ setRectangle(getNodeLocalBoundsRectangle(root), 0, 0, 1, 1);
235
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 1, 1);
236
+ setRectangle(getNodeLocalBoundsRectangle(grandchild), 0, 0, 2, 2);
237
+ child.x = 10;
238
+ grandchild.x = 20;
239
+ addNodeChild(root, child);
240
+ addNodeChild(child, grandchild);
241
+
242
+ const out = createRectangle();
243
+ computeNodeRootLocalBoundsRectangle(out, root);
244
+
245
+ expect(out).toMatchObject({ x: 0, y: 0, width: 32, height: 2 });
246
+ });
247
+
248
+ it('excludes disabled descendants', () => {
249
+ const root = createTestNode();
250
+ const child = createTestNode();
251
+ setRectangle(getNodeLocalBoundsRectangle(root), 0, 0, 10, 10);
252
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 20, 20);
253
+ child.x = 100;
254
+ child.enabled = false;
255
+ addNodeChild(root, child);
256
+
257
+ const out = createRectangle();
258
+ computeNodeRootLocalBoundsRectangle(out, root);
259
+
260
+ expect(out).toMatchObject({ x: 0, y: 0, width: 10, height: 10 });
261
+ });
200
262
  });
201
263
 
202
264
  describe('ensureNodeLocalBoundsRectangle', () => {
@@ -316,6 +378,20 @@ describe('ensureNodeWorldBoundsRectangle', () => {
316
378
  expect(runtime.worldBoundsRectangle).not.toBeNull();
317
379
  });
318
380
 
381
+ it('recomputes when bounds are cached before the world matrix exists', () => {
382
+ const object = createTestNode();
383
+ const runtime = getEntityRuntime(object);
384
+ getNodeLocalBoundsRectangle(object);
385
+ runtime.worldBoundsRectangle = createRectangle(5, 5, 10, 10);
386
+ runtime.worldBoundsUsingLocalBoundsId = runtime.localBoundsId;
387
+ expect(runtime.worldMatrix).toBeNull();
388
+
389
+ ensureNodeWorldBoundsRectangle(object);
390
+
391
+ expect(runtime.worldMatrix).not.toBeNull();
392
+ expect(runtime.worldBoundsRectangle).toMatchObject({ x: 0, y: 0, width: 0, height: 0 });
393
+ });
394
+
319
395
  it('should not recalculate if localBoundsId and worldTransformId are unchanged', () => {
320
396
  const object = createTestNode();
321
397
  const runtime = getEntityRuntime(object);
@@ -500,6 +576,20 @@ describe('getNodeWorldBoundsRectangle', () => {
500
576
  expect(bounds.width).toBeGreaterThan(10);
501
577
  expect(bounds.height).toBeGreaterThan(10);
502
578
  });
579
+
580
+ it('ignores an enabled child with zero-area world bounds', () => {
581
+ const parent = createTestNode();
582
+ const child = createTestNode();
583
+ addNodeChild(parent, child);
584
+ setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
585
+ setRectangle(getNodeLocalBoundsRectangle(child) as Rectangle, 0, 0, 0, 5);
586
+ child.x = 100;
587
+ invalidateNodeLocalTransform(child);
588
+
589
+ const bounds = getNodeWorldBoundsRectangle(parent);
590
+
591
+ expect(bounds).toMatchObject({ x: 0, y: 0, width: 10, height: 10 });
592
+ });
503
593
  });
504
594
 
505
595
  describe('setNodeHeight', () => {
@@ -67,6 +67,13 @@ describe('addNodeChild', () => {
67
67
  expect(() => addNodeChild(container, container)).toThrow(TypeError);
68
68
  });
69
69
 
70
+ it('rejects a child disallowed by the parent runtime', () => {
71
+ getEntityRuntime(container).canAddChild = () => false;
72
+
73
+ expect(() => addNodeChild(container, childA)).toThrow('The specified parent object cannot add this child');
74
+ expect(getNodeParent(childA)).toBeNull();
75
+ });
76
+
70
77
  it('removes child from previous parent before adding', () => {
71
78
  const other = createNode(NodeKind);
72
79
 
@@ -222,6 +229,15 @@ describe('addNodeChildAt', () => {
222
229
  expect(getNodeParentReferenceRevision(childA)).toBe(parentReferenceId);
223
230
  });
224
231
 
232
+ it('repairs a stale parent reference whose child-list entry is absent', () => {
233
+ getEntityRuntime(childA).parent = container;
234
+
235
+ addNodeChildAt(container, childA, 0);
236
+
237
+ expect(getNodeChildAt(container, 0)).toBe(childA);
238
+ expect(getNodeParent(childA)).toBe(container);
239
+ });
240
+
225
241
  it('calls onParentChanged on the child', () => {
226
242
  let called = false;
227
243
  connectSignal(enableNodeSignals(childA).onParentChanged, () => {
@@ -531,6 +547,16 @@ describe('removeNodeChild', () => {
531
547
  expect(getNodeParent(childA)).toBeNull();
532
548
  });
533
549
 
550
+ it('clears a stale parent reference when the child-list entry is absent', () => {
551
+ getEntityRuntime(container).children = [];
552
+ getEntityRuntime(childA).parent = container;
553
+
554
+ removeNodeChild(container, childA);
555
+
556
+ expect(getNodeParent(childA)).toBeNull();
557
+ expect(getNodeChildCount(container)).toBe(0);
558
+ });
559
+
534
560
  it('calls onParentChanged on the child', () => {
535
561
  addNodeChild(container, childA);
536
562
  let called = false;
@@ -605,6 +631,11 @@ describe('removeNodeChildAt', () => {
605
631
  });
606
632
 
607
633
  describe('removeNodeChildren', () => {
634
+ it('does nothing when the node has never had children', () => {
635
+ expect(() => removeNodeChildren(container)).not.toThrow();
636
+ expect(getNodeChildCount(container)).toBe(0);
637
+ });
638
+
608
639
  it('removeChildren removes all children by default', () => {
609
640
  addNodeChild(container, childA);
610
641
  addNodeChild(container, childB);
@@ -908,6 +939,11 @@ describe('replaceNodeChild', () => {
908
939
  });
909
940
 
910
941
  describe('setNodeChildIndex', () => {
942
+ it('does nothing when the node has never had children', () => {
943
+ expect(() => setNodeChildIndex(container, childA, 0)).not.toThrow();
944
+ expect(getNodeChildCount(container)).toBe(0);
945
+ });
946
+
911
947
  it('advances the child-list revision when order changes', () => {
912
948
  addNodeChild(container, childA);
913
949
  addNodeChild(container, childB);
@@ -948,6 +984,25 @@ describe('setNodeChildIndex', () => {
948
984
  expect(getChildren(container)[0]).toBe(childA);
949
985
  });
950
986
 
987
+ it('does not advance the revision when the child is already at the destination', () => {
988
+ addNodeChild(container, childA);
989
+ const childrenId = getNodeChildrenRevision(container);
990
+
991
+ setNodeChildIndex(container, childA, 0);
992
+
993
+ expect(getNodeChildrenRevision(container)).toBe(childrenId);
994
+ expect(getNodeChildAt(container, 0)).toBe(childA);
995
+ });
996
+
997
+ it('ignores a stale parent reference whose child-list entry is absent', () => {
998
+ addNodeChild(container, childB);
999
+ getEntityRuntime(childA).parent = container;
1000
+
1001
+ setNodeChildIndex(container, childA, 0);
1002
+
1003
+ expect(getChildren(container)).toEqual([childB]);
1004
+ });
1005
+
951
1006
  it('calls onChildrenOrderChanged on the parent', () => {
952
1007
  addNodeChild(container, childA);
953
1008
  addNodeChild(container, childB);
@@ -962,6 +1017,16 @@ describe('setNodeChildIndex', () => {
962
1017
  });
963
1018
 
964
1019
  describe('swapNodeChildren', () => {
1020
+ it('does not advance the revision when both arguments are the same child', () => {
1021
+ addNodeChild(container, childA);
1022
+ const childrenId = getNodeChildrenRevision(container);
1023
+
1024
+ swapNodeChildren(container, childA, childA);
1025
+
1026
+ expect(getNodeChildrenRevision(container)).toBe(childrenId);
1027
+ expect(getNodeChildAt(container, 0)).toBe(childA);
1028
+ });
1029
+
965
1030
  it('advances the child-list revision when order changes', () => {
966
1031
  addNodeChild(container, childA);
967
1032
  addNodeChild(container, childB);
@@ -1007,6 +1072,21 @@ describe('swapNodeChildren', () => {
1007
1072
  });
1008
1073
 
1009
1074
  describe('swapNodeChildrenAt', () => {
1075
+ it('does nothing when the node has never had children', () => {
1076
+ expect(() => swapNodeChildrenAt(container, 0, 1)).not.toThrow();
1077
+ expect(getNodeChildCount(container)).toBe(0);
1078
+ });
1079
+
1080
+ it('does not advance the revision when both indices are equal', () => {
1081
+ addNodeChild(container, childA);
1082
+ const childrenId = getNodeChildrenRevision(container);
1083
+
1084
+ swapNodeChildrenAt(container, 0, 0);
1085
+
1086
+ expect(getNodeChildrenRevision(container)).toBe(childrenId);
1087
+ expect(getNodeChildAt(container, 0)).toBe(childA);
1088
+ });
1089
+
1010
1090
  it('advances the child-list revision when order changes', () => {
1011
1091
  addNodeChild(container, childA);
1012
1092
  addNodeChild(container, childB);
package/src/node.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { connectSignal } from '@flighthq/signals/contract';
2
- import type { Node, NodeData, NodeRuntime, PartialNode } from '@flighthq/types/contract';
2
+ import type { InteractionSignals, Node, NodeData, NodeRuntime, PartialNode } from '@flighthq/types/contract';
3
3
  import { NodeKind } from '@flighthq/types/contract';
4
4
 
5
5
  import { addNodeChild, getNodeChildCount, getNodeParent } from './hierarchy';
@@ -165,6 +165,16 @@ describe('disposeNode', () => {
165
165
  expect(getNodeSignals(node)).toBeNull();
166
166
  });
167
167
 
168
+ it('releases the interaction signal registry after disposal', () => {
169
+ const node = createNode(NodeKind);
170
+ const runtime = getNodeRuntime(node) as NodeRuntime;
171
+ runtime.interactionSignals = {} as InteractionSignals;
172
+
173
+ disposeNode(node);
174
+
175
+ expect(runtime.interactionSignals).toBeNull();
176
+ });
177
+
168
178
  it('is safe to call on a root node with no parent', () => {
169
179
  const node = createNode(NodeKind);
170
180
  expect(() => disposeNode(node)).not.toThrow();
@@ -19,6 +19,17 @@ describe('addNodeColorAdjustment', () => {
19
19
  expect(getNodeColorAdjustments(node)).toEqual([adjustment]);
20
20
  expect(getNodeRuntime(node).resolvedColorScaleBias?.redScale).toBeCloseTo(0x7f / 255);
21
21
  });
22
+
23
+ it('appends to an existing stack without replacing earlier adjustments', () => {
24
+ const node = createNode(NodeKind);
25
+ const first = createTintAdjustment(0xff0000ff);
26
+ const second = createTintAdjustment(0x00ff00ff);
27
+
28
+ addNodeColorAdjustment(node, first);
29
+ addNodeColorAdjustment(node, second);
30
+
31
+ expect(getNodeColorAdjustments(node)).toEqual([first, second]);
32
+ });
22
33
  });
23
34
 
24
35
  describe('getNodeColorAdjustments', () => {
@@ -614,4 +614,13 @@ describe('swapNodeOrderListEntries', () => {
614
614
 
615
615
  expect(getNodeOrderListEntrySortKey(list, childA)).toBe(1);
616
616
  });
617
+
618
+ it('does nothing when the first node has no entry', () => {
619
+ const list = createNodeOrderList();
620
+ addNodeOrderListEntry(list, childB, 2);
621
+
622
+ swapNodeOrderListEntries(list, childA, childB);
623
+
624
+ expect(getNodeOrderListEntrySortKey(list, childB)).toBe(2);
625
+ });
617
626
  });
@@ -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', () => {
@@ -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,
@@ -190,6 +190,12 @@ describe('getNodeNextSibling', () => {
190
190
  it('returns the next sibling', () => {
191
191
  expect(getNodeNextSibling(childA)).toBe(childB);
192
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
+ });
193
199
  });
194
200
 
195
201
  describe('getNodePreviousSibling', () => {
@@ -204,6 +210,12 @@ describe('getNodePreviousSibling', () => {
204
210
  it('returns the previous sibling', () => {
205
211
  expect(getNodePreviousSibling(childB)).toBe(childA);
206
212
  });
213
+
214
+ it('returns null for a stale parent whose child list is absent', () => {
215
+ (getNodeRuntime(root) as NodeRuntime).children = null;
216
+
217
+ expect(getNodePreviousSibling(childB)).toBeNull();
218
+ });
207
219
  });
208
220
 
209
221
  describe('walkNodeDescendants', () => {
@@ -242,4 +254,16 @@ describe('walkNodeDescendants', () => {
242
254
  expect(visited).not.toContain(grandchild);
243
255
  expect(visited).not.toContain(childB);
244
256
  });
257
+
258
+ it('propagates an early stop from a nested descendant', () => {
259
+ const visited: Node<NodeTraits>[] = [];
260
+
261
+ const result = walkNodeDescendants(root, (node) => {
262
+ visited.push(node);
263
+ return node !== grandchild;
264
+ });
265
+
266
+ expect(result).toBe(false);
267
+ expect(visited).toEqual([childA, grandchild]);
268
+ });
245
269
  });