@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.
- package/dist/boundsRectangle.d.ts +11 -0
- package/dist/boundsRectangle.d.ts.map +1 -1
- package/dist/boundsRectangle.js +110 -12
- package/dist/boundsRectangle.js.map +1 -1
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableNodeGuards.d.ts +4 -0
- package/dist/enableNodeGuards.d.ts.map +1 -0
- package/dist/enableNodeGuards.js +29 -0
- package/dist/enableNodeGuards.js.map +1 -0
- package/dist/hasAppearance.d.ts +2 -1
- package/dist/hasAppearance.d.ts.map +1 -1
- package/dist/hasAppearance.js +10 -0
- package/dist/hasAppearance.js.map +1 -1
- package/dist/hasBoundsRectangle.d.ts +1 -1
- package/dist/hasBoundsRectangle.d.ts.map +1 -1
- package/dist/hasBoundsRectangle.js +5 -0
- package/dist/hasBoundsRectangle.js.map +1 -1
- package/dist/hasTransform3d.d.ts.map +1 -1
- package/dist/hasTransform3d.js +11 -4
- package/dist/hasTransform3d.js.map +1 -1
- package/dist/hierarchy.d.ts +15 -7
- package/dist/hierarchy.d.ts.map +1 -1
- package/dist/hierarchy.js +65 -28
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -0
- package/dist/node.js.map +1 -1
- package/dist/nodeColorAdjustment.d.ts.map +1 -1
- package/dist/nodeColorAdjustment.js +1 -7
- package/dist/nodeColorAdjustment.js.map +1 -1
- package/dist/nodeOrderList.d.ts +88 -0
- package/dist/nodeOrderList.d.ts.map +1 -0
- package/dist/nodeOrderList.js +257 -0
- package/dist/nodeOrderList.js.map +1 -0
- package/dist/nodeTransform2d.d.ts.map +1 -1
- package/dist/nodeTransform2d.js +1 -1
- package/dist/nodeTransform2d.js.map +1 -1
- package/dist/revision.d.ts +5 -2
- package/dist/revision.d.ts.map +1 -1
- package/dist/revision.js +17 -5
- package/dist/revision.js.map +1 -1
- package/dist/stageFit.d.ts.map +1 -1
- package/dist/stageFit.js +8 -4
- package/dist/stageFit.js.map +1 -1
- package/dist/traversal.d.ts +1 -1
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +1 -1
- package/dist/traversal.js.map +1 -1
- package/package.json +11 -7
- package/src/boundsRectangle.test.ts +378 -7
- package/src/enableNodeGuards.test.ts +78 -0
- package/src/hasAppearance.test.ts +33 -3
- package/src/hasBoundsRectangle.test.ts +8 -0
- package/src/hasTransform2d.test.ts +6 -0
- package/src/hasTransform3d.test.ts +40 -8
- package/src/hierarchy.test.ts +298 -4
- package/src/node.test.ts +11 -1
- package/src/nodeColorAdjustment.test.ts +11 -0
- package/src/nodeOrderList.test.ts +683 -0
- package/src/nodeTransform2d.test.ts +45 -0
- package/src/revision.test.ts +51 -0
- package/src/stageFit.test.ts +56 -1
- package/src/traversal.test.ts +32 -3
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { createQuaternion, createVector3 } from '@flighthq/geometry/contract';
|
|
2
|
-
import type { HasTransform3D, HasTransform3DRuntime } from '@flighthq/types/contract';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { HasTransform3D, HasTransform3DRuntime, Node, NodeRuntime } from '@flighthq/types/contract';
|
|
4
3
|
|
|
5
4
|
import { initTransform3DRuntimeTrait, initTransform3DTrait } from './hasTransform3d';
|
|
5
|
+
import { createNode, createNodeRuntime } from './node';
|
|
6
6
|
|
|
7
7
|
describe('initTransform3DRuntimeTrait', () => {
|
|
8
|
+
let runtime: NodeRuntime<HasTransform3D> & HasTransform3DRuntime;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
runtime = createNodeRuntime() as NodeRuntime<HasTransform3D> & HasTransform3DRuntime;
|
|
12
|
+
});
|
|
13
|
+
|
|
8
14
|
it('nulls the matrix caches and clears the detached flag', () => {
|
|
9
|
-
const runtime = {} as HasTransform3DRuntime;
|
|
10
15
|
initTransform3DRuntimeTrait(runtime);
|
|
16
|
+
|
|
11
17
|
expect(runtime.localMatrix4).toBeNull();
|
|
12
18
|
expect(runtime.worldMatrix4).toBeNull();
|
|
13
19
|
expect(runtime.localMatrix4Detached).toBe(false);
|
|
@@ -15,22 +21,48 @@ describe('initTransform3DRuntimeTrait', () => {
|
|
|
15
21
|
});
|
|
16
22
|
|
|
17
23
|
describe('initTransform3DTrait', () => {
|
|
24
|
+
let node: HasTransform3D;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
node = createNode(NodeTestKind) as Node<HasTransform3D> & HasTransform3D;
|
|
28
|
+
});
|
|
29
|
+
|
|
18
30
|
it('defaults to identity position/rotation/scale', () => {
|
|
19
|
-
const node = {} as HasTransform3D;
|
|
20
31
|
initTransform3DTrait(node);
|
|
32
|
+
|
|
21
33
|
expect(node.position).toMatchObject({ x: 0, y: 0, z: 0 });
|
|
22
34
|
expect(node.rotation).toMatchObject({ x: 0, y: 0, z: 0, w: 1 });
|
|
23
35
|
expect(node.scale).toMatchObject({ x: 1, y: 1, z: 1 });
|
|
24
36
|
});
|
|
25
37
|
|
|
26
38
|
it('accepts existing position/rotation/scale', () => {
|
|
27
|
-
const node = {} as HasTransform3D;
|
|
28
39
|
const position = createVector3(7, 0, 0);
|
|
29
40
|
const rotation = createQuaternion();
|
|
30
41
|
const scale = createVector3(2, 2, 2);
|
|
31
42
|
initTransform3DTrait(node, { position, rotation, scale });
|
|
32
|
-
|
|
33
|
-
expect(node.
|
|
34
|
-
expect(node.
|
|
43
|
+
|
|
44
|
+
expect(node.position).toMatchObject({ x: 7, y: 0, z: 0 });
|
|
45
|
+
expect(node.rotation).toMatchObject({ x: 0, y: 0, z: 0, w: 1 });
|
|
46
|
+
expect(node.scale).toMatchObject({ x: 2, y: 2, z: 2 });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('copies out of the options object so two nodes never share storage', () => {
|
|
50
|
+
const options = { position: createVector3(1, 2, 3), rotation: createQuaternion(), scale: createVector3(2, 2, 2) };
|
|
51
|
+
const second = createNode(NodeTestKind) as Node<HasTransform3D> & HasTransform3D;
|
|
52
|
+
initTransform3DTrait(node, options);
|
|
53
|
+
initTransform3DTrait(second, options);
|
|
54
|
+
|
|
55
|
+
node.position.x = 999;
|
|
56
|
+
node.scale.y = 999;
|
|
57
|
+
node.rotation.w = 0;
|
|
58
|
+
|
|
59
|
+
expect(second.position.x).toBe(1);
|
|
60
|
+
expect(second.scale.y).toBe(2);
|
|
61
|
+
expect(second.rotation.w).toBe(1);
|
|
62
|
+
expect(options.position.x).toBe(1);
|
|
63
|
+
expect(options.scale.y).toBe(2);
|
|
64
|
+
expect(options.rotation.w).toBe(1);
|
|
35
65
|
});
|
|
36
66
|
});
|
|
67
|
+
|
|
68
|
+
const NodeTestKind = 'NodeTest';
|
package/src/hierarchy.test.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
removeNodeChildAt,
|
|
25
25
|
removeNodeChildren,
|
|
26
26
|
reparentNode,
|
|
27
|
+
setReparentNodeGuard,
|
|
27
28
|
replaceNodeChild,
|
|
28
29
|
setNodeChildIndex,
|
|
29
30
|
swapNodeChildren,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
} from './hierarchy';
|
|
32
33
|
import { createNode, enableNodeSignals, getNodeRuntime } from './node';
|
|
33
34
|
import { getNodeWorldMatrix } from './nodeTransform2d';
|
|
34
|
-
import { invalidateNodeLocalTransform } from './revision';
|
|
35
|
+
import { getNodeChildrenRevision, getNodeParentReferenceRevision, invalidateNodeLocalTransform } from './revision';
|
|
35
36
|
|
|
36
37
|
let container: Node<NodeTraits>;
|
|
37
38
|
let childA: Node<NodeTraits>;
|
|
@@ -67,6 +68,22 @@ describe('addNodeChild', () => {
|
|
|
67
68
|
expect(() => addNodeChild(container, container)).toThrow(TypeError);
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
it('throws rather than creating a cycle', () => {
|
|
72
|
+
addNodeChild(container, childA);
|
|
73
|
+
addNodeChild(childA, childB);
|
|
74
|
+
|
|
75
|
+
expect(() => addNodeChild(childB, container)).toThrow(TypeError);
|
|
76
|
+
expect(getNodeParent(container)).toBeNull();
|
|
77
|
+
expect(getNodeRoot(childB)).toBe(container);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects a child disallowed by the parent runtime', () => {
|
|
81
|
+
getEntityRuntime(container).canAddChild = () => false;
|
|
82
|
+
|
|
83
|
+
expect(() => addNodeChild(container, childA)).toThrow('The specified parent object cannot add this child');
|
|
84
|
+
expect(getNodeParent(childA)).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
70
87
|
it('removes child from previous parent before adding', () => {
|
|
71
88
|
const other = createNode(NodeKind);
|
|
72
89
|
|
|
@@ -153,6 +170,16 @@ describe('addNodeChild', () => {
|
|
|
153
170
|
});
|
|
154
171
|
|
|
155
172
|
describe('addNodeChildAt', () => {
|
|
173
|
+
it('advances structural revisions when attaching a child', () => {
|
|
174
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
175
|
+
const parentReferenceId = getNodeParentReferenceRevision(childA);
|
|
176
|
+
|
|
177
|
+
addNodeChildAt(container, childA, 0);
|
|
178
|
+
|
|
179
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId + 1);
|
|
180
|
+
expect(getNodeParentReferenceRevision(childA)).toBe(parentReferenceId + 1);
|
|
181
|
+
});
|
|
182
|
+
|
|
156
183
|
it('addNodeChildAt inserts a child at the given index', () => {
|
|
157
184
|
addNodeChild(container, childA);
|
|
158
185
|
addNodeChildAt(container, childB, 0);
|
|
@@ -189,6 +216,38 @@ describe('addNodeChildAt', () => {
|
|
|
189
216
|
expect(getChildren(container)[1]).toBe(childA);
|
|
190
217
|
});
|
|
191
218
|
|
|
219
|
+
it('advances only the child-list revision when reordering within one parent', () => {
|
|
220
|
+
addNodeChild(container, childA);
|
|
221
|
+
addNodeChild(container, childB);
|
|
222
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
223
|
+
const parentReferenceId = getNodeParentReferenceRevision(childA);
|
|
224
|
+
|
|
225
|
+
addNodeChildAt(container, childA, 1);
|
|
226
|
+
|
|
227
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId + 1);
|
|
228
|
+
expect(getNodeParentReferenceRevision(childA)).toBe(parentReferenceId);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('does not advance structural revisions for an unchanged index', () => {
|
|
232
|
+
addNodeChild(container, childA);
|
|
233
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
234
|
+
const parentReferenceId = getNodeParentReferenceRevision(childA);
|
|
235
|
+
|
|
236
|
+
addNodeChildAt(container, childA, 0);
|
|
237
|
+
|
|
238
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId);
|
|
239
|
+
expect(getNodeParentReferenceRevision(childA)).toBe(parentReferenceId);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('repairs a stale parent reference whose child-list entry is absent', () => {
|
|
243
|
+
getEntityRuntime(childA).parent = container;
|
|
244
|
+
|
|
245
|
+
addNodeChildAt(container, childA, 0);
|
|
246
|
+
|
|
247
|
+
expect(getNodeChildAt(container, 0)).toBe(childA);
|
|
248
|
+
expect(getNodeParent(childA)).toBe(container);
|
|
249
|
+
});
|
|
250
|
+
|
|
192
251
|
it('calls onParentChanged on the child', () => {
|
|
193
252
|
let called = false;
|
|
194
253
|
connectSignal(enableNodeSignals(childA).onParentChanged, () => {
|
|
@@ -456,6 +515,17 @@ describe('isNodeAncestorOf', () => {
|
|
|
456
515
|
});
|
|
457
516
|
|
|
458
517
|
describe('removeNodeChild', () => {
|
|
518
|
+
it('advances structural revisions when detaching a child', () => {
|
|
519
|
+
addNodeChild(container, childA);
|
|
520
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
521
|
+
const parentReferenceId = getNodeParentReferenceRevision(childA);
|
|
522
|
+
|
|
523
|
+
removeNodeChild(container, childA);
|
|
524
|
+
|
|
525
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId + 1);
|
|
526
|
+
expect(getNodeParentReferenceRevision(childA)).toBe(parentReferenceId + 1);
|
|
527
|
+
});
|
|
528
|
+
|
|
459
529
|
it('removes the child and clears its parent', () => {
|
|
460
530
|
addNodeChild(container, childA);
|
|
461
531
|
expect(getNodeChildCount(container)).toBe(1);
|
|
@@ -487,6 +557,16 @@ describe('removeNodeChild', () => {
|
|
|
487
557
|
expect(getNodeParent(childA)).toBeNull();
|
|
488
558
|
});
|
|
489
559
|
|
|
560
|
+
it('clears a stale parent reference when the child-list entry is absent', () => {
|
|
561
|
+
getEntityRuntime(container).children = [];
|
|
562
|
+
getEntityRuntime(childA).parent = container;
|
|
563
|
+
|
|
564
|
+
removeNodeChild(container, childA);
|
|
565
|
+
|
|
566
|
+
expect(getNodeParent(childA)).toBeNull();
|
|
567
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
568
|
+
});
|
|
569
|
+
|
|
490
570
|
it('calls onParentChanged on the child', () => {
|
|
491
571
|
addNodeChild(container, childA);
|
|
492
572
|
let called = false;
|
|
@@ -561,6 +641,11 @@ describe('removeNodeChildAt', () => {
|
|
|
561
641
|
});
|
|
562
642
|
|
|
563
643
|
describe('removeNodeChildren', () => {
|
|
644
|
+
it('does nothing when the node has never had children', () => {
|
|
645
|
+
expect(() => removeNodeChildren(container)).not.toThrow();
|
|
646
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
647
|
+
});
|
|
648
|
+
|
|
564
649
|
it('removeChildren removes all children by default', () => {
|
|
565
650
|
addNodeChild(container, childA);
|
|
566
651
|
addNodeChild(container, childB);
|
|
@@ -637,6 +722,50 @@ describe('reparentNode', () => {
|
|
|
637
722
|
return node;
|
|
638
723
|
}
|
|
639
724
|
|
|
725
|
+
// The ruled contract: under a singular new parent, NO local transform exists that would preserve the
|
|
726
|
+
// child's world transform, so the function declines whole rather than inventing one. These assert
|
|
727
|
+
// BOTH halves stay unchanged — a partial success is the state the ruling exists to prevent.
|
|
728
|
+
it('declines whole when the new parent has no invertible world matrix', () => {
|
|
729
|
+
const parentA = createTransformNode();
|
|
730
|
+
const child = createTransformNode();
|
|
731
|
+
child.x = 20;
|
|
732
|
+
child.y = 10;
|
|
733
|
+
invalidateNodeLocalTransform(child);
|
|
734
|
+
addNodeChild(parentA, child);
|
|
735
|
+
|
|
736
|
+
const collapsed = createTransformNode();
|
|
737
|
+
collapsed.scaleX = 0;
|
|
738
|
+
invalidateNodeLocalTransform(collapsed);
|
|
739
|
+
|
|
740
|
+
expect(reparentNode(child, collapsed)).toBe(false);
|
|
741
|
+
// Neither half happened: the attachment is untouched...
|
|
742
|
+
expect(getNodeParent(child)).toBe(parentA);
|
|
743
|
+
expect(getNodeChildCount(collapsed)).toBe(0);
|
|
744
|
+
// ...and so is the transform, with no NaN written into it.
|
|
745
|
+
expect(child.x).toBe(20);
|
|
746
|
+
expect(child.y).toBe(10);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('reports true and still preserves world position for an invertible parent', () => {
|
|
750
|
+
const parentA = createTransformNode();
|
|
751
|
+
const child = createTransformNode();
|
|
752
|
+
child.x = 20;
|
|
753
|
+
child.y = 10;
|
|
754
|
+
invalidateNodeLocalTransform(child);
|
|
755
|
+
addNodeChild(parentA, child);
|
|
756
|
+
const before = cloneMatrix(getNodeWorldMatrix(child));
|
|
757
|
+
|
|
758
|
+
const parentB = createTransformNode();
|
|
759
|
+
parentB.x = 200;
|
|
760
|
+
invalidateNodeLocalTransform(parentB);
|
|
761
|
+
|
|
762
|
+
expect(reparentNode(child, parentB)).toBe(true);
|
|
763
|
+
expect(getNodeParent(child)).toBe(parentB);
|
|
764
|
+
const after = getNodeWorldMatrix(child);
|
|
765
|
+
expect(after.tx).toBeCloseTo(before.tx, 6);
|
|
766
|
+
expect(after.ty).toBeCloseTo(before.ty, 6);
|
|
767
|
+
});
|
|
768
|
+
|
|
640
769
|
it('preserves world position after reparenting', () => {
|
|
641
770
|
const parentA = createTransformNode();
|
|
642
771
|
parentA.x = 100;
|
|
@@ -696,27 +825,81 @@ describe('reparentNode', () => {
|
|
|
696
825
|
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
697
826
|
});
|
|
698
827
|
|
|
699
|
-
it('preserves
|
|
828
|
+
it('preserves shear induced by rotation across non-uniformly scaled parents', () => {
|
|
829
|
+
const parentA = createTransformNode();
|
|
830
|
+
parentA.rotation = -15;
|
|
831
|
+
parentA.scaleX = 0.75;
|
|
832
|
+
parentA.scaleY = 2.5;
|
|
833
|
+
invalidateNodeLocalTransform(parentA);
|
|
834
|
+
|
|
835
|
+
const child = createTransformNode();
|
|
836
|
+
child.x = 10;
|
|
837
|
+
child.y = 5;
|
|
838
|
+
child.rotation = 37;
|
|
839
|
+
child.scaleX = 1.2;
|
|
840
|
+
child.scaleY = 0.9;
|
|
841
|
+
invalidateNodeLocalTransform(child);
|
|
842
|
+
addNodeChild(parentA, child);
|
|
843
|
+
|
|
844
|
+
expect(child.skewX).toBe(0);
|
|
845
|
+
expect(child.skewY).toBe(0);
|
|
846
|
+
const before = cloneMatrix(getNodeWorldMatrix(child));
|
|
847
|
+
|
|
848
|
+
const parentB = createTransformNode();
|
|
849
|
+
parentB.rotation = 22;
|
|
850
|
+
parentB.scaleX = 2.4;
|
|
851
|
+
parentB.scaleY = 0.6;
|
|
852
|
+
invalidateNodeLocalTransform(parentB);
|
|
853
|
+
|
|
854
|
+
reparentNode(child, parentB);
|
|
855
|
+
const after = getNodeWorldMatrix(child);
|
|
856
|
+
|
|
857
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
858
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
859
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
860
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
861
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
862
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it('preserves a skewed world transform across asymmetric parents', () => {
|
|
700
866
|
const parentA = createTransformNode();
|
|
701
867
|
parentA.x = 50;
|
|
702
868
|
parentA.rotation = 45;
|
|
869
|
+
parentA.scaleX = 2;
|
|
870
|
+
parentA.scaleY = 3;
|
|
703
871
|
invalidateNodeLocalTransform(parentA);
|
|
704
872
|
|
|
705
873
|
const child = createTransformNode();
|
|
706
874
|
child.x = 10;
|
|
875
|
+
child.y = 5;
|
|
876
|
+
child.rotation = 12;
|
|
877
|
+
child.scaleX = 1.4;
|
|
878
|
+
child.scaleY = 0.8;
|
|
707
879
|
child.skewX = 15;
|
|
708
880
|
child.skewY = 20;
|
|
709
881
|
invalidateNodeLocalTransform(child);
|
|
710
882
|
addNodeChild(parentA, child);
|
|
711
883
|
|
|
884
|
+
const before = cloneMatrix(getNodeWorldMatrix(child));
|
|
885
|
+
|
|
712
886
|
const parentB = createTransformNode();
|
|
713
887
|
parentB.x = 100;
|
|
888
|
+
parentB.y = -20;
|
|
889
|
+
parentB.rotation = -10;
|
|
890
|
+
parentB.scaleX = 0.7;
|
|
891
|
+
parentB.scaleY = 1.6;
|
|
714
892
|
invalidateNodeLocalTransform(parentB);
|
|
715
893
|
|
|
716
894
|
reparentNode(child, parentB);
|
|
895
|
+
const after = getNodeWorldMatrix(child);
|
|
717
896
|
|
|
718
|
-
expect(
|
|
719
|
-
expect(
|
|
897
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
898
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
899
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
900
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
901
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
902
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
720
903
|
});
|
|
721
904
|
|
|
722
905
|
it('preserves world position with pivot', () => {
|
|
@@ -798,6 +981,7 @@ describe('reparentNode', () => {
|
|
|
798
981
|
reparentNode(child, parentB);
|
|
799
982
|
const after = getNodeWorldMatrix(child);
|
|
800
983
|
|
|
984
|
+
expect(child.scaleY).toBeLessThan(0);
|
|
801
985
|
expect(after.a).toBeCloseTo(before.a, 10);
|
|
802
986
|
expect(after.b).toBeCloseTo(before.b, 10);
|
|
803
987
|
expect(after.c).toBeCloseTo(before.c, 10);
|
|
@@ -864,6 +1048,21 @@ describe('replaceNodeChild', () => {
|
|
|
864
1048
|
});
|
|
865
1049
|
|
|
866
1050
|
describe('setNodeChildIndex', () => {
|
|
1051
|
+
it('does nothing when the node has never had children', () => {
|
|
1052
|
+
expect(() => setNodeChildIndex(container, childA, 0)).not.toThrow();
|
|
1053
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('advances the child-list revision when order changes', () => {
|
|
1057
|
+
addNodeChild(container, childA);
|
|
1058
|
+
addNodeChild(container, childB);
|
|
1059
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
1060
|
+
|
|
1061
|
+
setNodeChildIndex(container, childA, 1);
|
|
1062
|
+
|
|
1063
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId + 1);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
867
1066
|
it('setChildIndex moves an existing child to a new index', () => {
|
|
868
1067
|
addNodeChild(container, childA);
|
|
869
1068
|
addNodeChild(container, childB);
|
|
@@ -894,6 +1093,25 @@ describe('setNodeChildIndex', () => {
|
|
|
894
1093
|
expect(getChildren(container)[0]).toBe(childA);
|
|
895
1094
|
});
|
|
896
1095
|
|
|
1096
|
+
it('does not advance the revision when the child is already at the destination', () => {
|
|
1097
|
+
addNodeChild(container, childA);
|
|
1098
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
1099
|
+
|
|
1100
|
+
setNodeChildIndex(container, childA, 0);
|
|
1101
|
+
|
|
1102
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId);
|
|
1103
|
+
expect(getNodeChildAt(container, 0)).toBe(childA);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('ignores a stale parent reference whose child-list entry is absent', () => {
|
|
1107
|
+
addNodeChild(container, childB);
|
|
1108
|
+
getEntityRuntime(childA).parent = container;
|
|
1109
|
+
|
|
1110
|
+
setNodeChildIndex(container, childA, 0);
|
|
1111
|
+
|
|
1112
|
+
expect(getChildren(container)).toEqual([childB]);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
897
1115
|
it('calls onChildrenOrderChanged on the parent', () => {
|
|
898
1116
|
addNodeChild(container, childA);
|
|
899
1117
|
addNodeChild(container, childB);
|
|
@@ -907,7 +1125,58 @@ describe('setNodeChildIndex', () => {
|
|
|
907
1125
|
});
|
|
908
1126
|
});
|
|
909
1127
|
|
|
1128
|
+
describe('setReparentNodeGuard', () => {
|
|
1129
|
+
type TransformNode = Node<HasTransform2D> & HasTransform2D;
|
|
1130
|
+
function createTransformNode(): TransformNode {
|
|
1131
|
+
const node = createNode('TransformTest') as TransformNode;
|
|
1132
|
+
initTransform2DTrait(node);
|
|
1133
|
+
initTransform2DRuntimeTrait(getRawEntityRuntime(node) as HasTransform2DRuntime);
|
|
1134
|
+
return node;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
it('is notified on the decline and never on success', () => {
|
|
1138
|
+
const seen: string[] = [];
|
|
1139
|
+
setReparentNodeGuard((_child, newParent) => seen.push(newParent.name ?? '<unnamed>'));
|
|
1140
|
+
try {
|
|
1141
|
+
const child = createTransformNode();
|
|
1142
|
+
const collapsed = createTransformNode();
|
|
1143
|
+
collapsed.name = 'collapsed';
|
|
1144
|
+
collapsed.scaleX = 0;
|
|
1145
|
+
invalidateNodeLocalTransform(collapsed);
|
|
1146
|
+
const healthy = createTransformNode();
|
|
1147
|
+
healthy.name = 'healthy';
|
|
1148
|
+
invalidateNodeLocalTransform(healthy);
|
|
1149
|
+
|
|
1150
|
+
reparentNode(child, collapsed);
|
|
1151
|
+
reparentNode(child, healthy);
|
|
1152
|
+
expect(seen).toEqual(['collapsed']);
|
|
1153
|
+
} finally {
|
|
1154
|
+
setReparentNodeGuard(null);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
910
1159
|
describe('swapNodeChildren', () => {
|
|
1160
|
+
it('does not advance the revision when both arguments are the same child', () => {
|
|
1161
|
+
addNodeChild(container, childA);
|
|
1162
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
1163
|
+
|
|
1164
|
+
swapNodeChildren(container, childA, childA);
|
|
1165
|
+
|
|
1166
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId);
|
|
1167
|
+
expect(getNodeChildAt(container, 0)).toBe(childA);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it('advances the child-list revision when order changes', () => {
|
|
1171
|
+
addNodeChild(container, childA);
|
|
1172
|
+
addNodeChild(container, childB);
|
|
1173
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
1174
|
+
|
|
1175
|
+
swapNodeChildren(container, childA, childB);
|
|
1176
|
+
|
|
1177
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId + 1);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
911
1180
|
it('swapNodeChildren swaps two children', () => {
|
|
912
1181
|
addNodeChild(container, childA);
|
|
913
1182
|
addNodeChild(container, childB);
|
|
@@ -943,6 +1212,31 @@ describe('swapNodeChildren', () => {
|
|
|
943
1212
|
});
|
|
944
1213
|
|
|
945
1214
|
describe('swapNodeChildrenAt', () => {
|
|
1215
|
+
it('does nothing when the node has never had children', () => {
|
|
1216
|
+
expect(() => swapNodeChildrenAt(container, 0, 1)).not.toThrow();
|
|
1217
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it('does not advance the revision when both indices are equal', () => {
|
|
1221
|
+
addNodeChild(container, childA);
|
|
1222
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
1223
|
+
|
|
1224
|
+
swapNodeChildrenAt(container, 0, 0);
|
|
1225
|
+
|
|
1226
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId);
|
|
1227
|
+
expect(getNodeChildAt(container, 0)).toBe(childA);
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
it('advances the child-list revision when order changes', () => {
|
|
1231
|
+
addNodeChild(container, childA);
|
|
1232
|
+
addNodeChild(container, childB);
|
|
1233
|
+
const childrenId = getNodeChildrenRevision(container);
|
|
1234
|
+
|
|
1235
|
+
swapNodeChildrenAt(container, 0, 1);
|
|
1236
|
+
|
|
1237
|
+
expect(getNodeChildrenRevision(container)).toBe(childrenId + 1);
|
|
1238
|
+
});
|
|
1239
|
+
|
|
946
1240
|
it('swapNodeChildrenAt swaps children by index', () => {
|
|
947
1241
|
addNodeChild(container, childA);
|
|
948
1242
|
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', () => {
|