@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.
- package/dist/nodeColorAdjustment.d.ts.map +1 -1
- package/dist/nodeColorAdjustment.js +1 -7
- package/dist/nodeColorAdjustment.js.map +1 -1
- package/package.json +7 -7
- package/src/boundsRectangle.test.ts +92 -2
- package/src/hierarchy.test.ts +80 -0
- package/src/node.test.ts +11 -1
- package/src/nodeColorAdjustment.test.ts +11 -0
- package/src/nodeOrderList.test.ts +9 -0
- package/src/nodeTransform2d.test.ts +22 -0
- package/src/traversal.test.ts +26 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodeColorAdjustment.d.ts","sourceRoot":"","sources":["../src/nodeColorAdjustment.ts"],"names":[],"mappings":"
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
42
|
-
"@flighthq/entity": "0.3.1-next.
|
|
43
|
-
"@flighthq/geometry": "0.3.1-next.
|
|
44
|
-
"@flighthq/materials": "0.3.1-next.
|
|
45
|
-
"@flighthq/signals": "0.3.1-next.
|
|
46
|
-
"@flighthq/types": "0.3.1-next.
|
|
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> &
|
|
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', () => {
|
package/src/hierarchy.test.ts
CHANGED
|
@@ -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', () => {
|
package/src/traversal.test.ts
CHANGED
|
@@ -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
|
});
|