@flighthq/node 0.3.0 → 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 +3 -3
- package/dist/boundsRectangle.d.ts.map +1 -1
- package/dist/boundsRectangle.js +63 -11
- package/dist/boundsRectangle.js.map +1 -1
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -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.map +1 -1
- package/dist/hasBoundsRectangle.js +4 -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 +46 -25
- 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/nodeColorAdjustment.d.ts.map +1 -1
- package/dist/nodeColorAdjustment.js +1 -7
- package/dist/nodeColorAdjustment.js.map +1 -1
- package/dist/nodeOrderList.d.ts.map +1 -1
- package/dist/nodeOrderList.js +9 -2
- package/dist/nodeOrderList.js.map +1 -1
- 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 +2 -1
- package/dist/revision.d.ts.map +1 -1
- package/dist/revision.js +9 -4
- package/dist/revision.js.map +1 -1
- package/dist/stageFit.d.ts.map +1 -1
- package/dist/stageFit.js +8 -2
- package/dist/stageFit.js.map +1 -1
- package/package.json +9 -7
- package/src/boundsRectangle.test.ts +320 -7
- package/src/enableNodeGuards.test.ts +78 -0
- package/src/hasAppearance.test.ts +33 -3
- package/src/hasTransform2d.test.ts +6 -0
- package/src/hasTransform3d.test.ts +40 -8
- package/src/hierarchy.test.ts +223 -3
- package/src/node.test.ts +11 -1
- package/src/nodeColorAdjustment.test.ts +11 -0
- package/src/nodeOrderList.test.ts +84 -18
- package/src/nodeTransform2d.test.ts +45 -0
- package/src/revision.test.ts +26 -0
- package/src/stageFit.test.ts +56 -1
- package/src/traversal.test.ts +32 -2
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,
|
|
@@ -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
|
|
|
@@ -222,6 +239,15 @@ describe('addNodeChildAt', () => {
|
|
|
222
239
|
expect(getNodeParentReferenceRevision(childA)).toBe(parentReferenceId);
|
|
223
240
|
});
|
|
224
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
|
+
|
|
225
251
|
it('calls onParentChanged on the child', () => {
|
|
226
252
|
let called = false;
|
|
227
253
|
connectSignal(enableNodeSignals(childA).onParentChanged, () => {
|
|
@@ -531,6 +557,16 @@ describe('removeNodeChild', () => {
|
|
|
531
557
|
expect(getNodeParent(childA)).toBeNull();
|
|
532
558
|
});
|
|
533
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
|
+
|
|
534
570
|
it('calls onParentChanged on the child', () => {
|
|
535
571
|
addNodeChild(container, childA);
|
|
536
572
|
let called = false;
|
|
@@ -605,6 +641,11 @@ describe('removeNodeChildAt', () => {
|
|
|
605
641
|
});
|
|
606
642
|
|
|
607
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
|
+
|
|
608
649
|
it('removeChildren removes all children by default', () => {
|
|
609
650
|
addNodeChild(container, childA);
|
|
610
651
|
addNodeChild(container, childB);
|
|
@@ -681,6 +722,50 @@ describe('reparentNode', () => {
|
|
|
681
722
|
return node;
|
|
682
723
|
}
|
|
683
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
|
+
|
|
684
769
|
it('preserves world position after reparenting', () => {
|
|
685
770
|
const parentA = createTransformNode();
|
|
686
771
|
parentA.x = 100;
|
|
@@ -740,27 +825,81 @@ describe('reparentNode', () => {
|
|
|
740
825
|
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
741
826
|
});
|
|
742
827
|
|
|
743
|
-
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', () => {
|
|
744
866
|
const parentA = createTransformNode();
|
|
745
867
|
parentA.x = 50;
|
|
746
868
|
parentA.rotation = 45;
|
|
869
|
+
parentA.scaleX = 2;
|
|
870
|
+
parentA.scaleY = 3;
|
|
747
871
|
invalidateNodeLocalTransform(parentA);
|
|
748
872
|
|
|
749
873
|
const child = createTransformNode();
|
|
750
874
|
child.x = 10;
|
|
875
|
+
child.y = 5;
|
|
876
|
+
child.rotation = 12;
|
|
877
|
+
child.scaleX = 1.4;
|
|
878
|
+
child.scaleY = 0.8;
|
|
751
879
|
child.skewX = 15;
|
|
752
880
|
child.skewY = 20;
|
|
753
881
|
invalidateNodeLocalTransform(child);
|
|
754
882
|
addNodeChild(parentA, child);
|
|
755
883
|
|
|
884
|
+
const before = cloneMatrix(getNodeWorldMatrix(child));
|
|
885
|
+
|
|
756
886
|
const parentB = createTransformNode();
|
|
757
887
|
parentB.x = 100;
|
|
888
|
+
parentB.y = -20;
|
|
889
|
+
parentB.rotation = -10;
|
|
890
|
+
parentB.scaleX = 0.7;
|
|
891
|
+
parentB.scaleY = 1.6;
|
|
758
892
|
invalidateNodeLocalTransform(parentB);
|
|
759
893
|
|
|
760
894
|
reparentNode(child, parentB);
|
|
895
|
+
const after = getNodeWorldMatrix(child);
|
|
761
896
|
|
|
762
|
-
expect(
|
|
763
|
-
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);
|
|
764
903
|
});
|
|
765
904
|
|
|
766
905
|
it('preserves world position with pivot', () => {
|
|
@@ -842,6 +981,7 @@ describe('reparentNode', () => {
|
|
|
842
981
|
reparentNode(child, parentB);
|
|
843
982
|
const after = getNodeWorldMatrix(child);
|
|
844
983
|
|
|
984
|
+
expect(child.scaleY).toBeLessThan(0);
|
|
845
985
|
expect(after.a).toBeCloseTo(before.a, 10);
|
|
846
986
|
expect(after.b).toBeCloseTo(before.b, 10);
|
|
847
987
|
expect(after.c).toBeCloseTo(before.c, 10);
|
|
@@ -908,6 +1048,11 @@ describe('replaceNodeChild', () => {
|
|
|
908
1048
|
});
|
|
909
1049
|
|
|
910
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
|
+
|
|
911
1056
|
it('advances the child-list revision when order changes', () => {
|
|
912
1057
|
addNodeChild(container, childA);
|
|
913
1058
|
addNodeChild(container, childB);
|
|
@@ -948,6 +1093,25 @@ describe('setNodeChildIndex', () => {
|
|
|
948
1093
|
expect(getChildren(container)[0]).toBe(childA);
|
|
949
1094
|
});
|
|
950
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
|
+
|
|
951
1115
|
it('calls onChildrenOrderChanged on the parent', () => {
|
|
952
1116
|
addNodeChild(container, childA);
|
|
953
1117
|
addNodeChild(container, childB);
|
|
@@ -961,7 +1125,48 @@ describe('setNodeChildIndex', () => {
|
|
|
961
1125
|
});
|
|
962
1126
|
});
|
|
963
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
|
+
|
|
964
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
|
+
|
|
965
1170
|
it('advances the child-list revision when order changes', () => {
|
|
966
1171
|
addNodeChild(container, childA);
|
|
967
1172
|
addNodeChild(container, childB);
|
|
@@ -1007,6 +1212,21 @@ describe('swapNodeChildren', () => {
|
|
|
1007
1212
|
});
|
|
1008
1213
|
|
|
1009
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
|
+
|
|
1010
1230
|
it('advances the child-list revision when order changes', () => {
|
|
1011
1231
|
addNodeChild(container, childA);
|
|
1012
1232
|
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', () => {
|
|
@@ -38,6 +38,12 @@ function getChildren(source: Node) {
|
|
|
38
38
|
return getNodeRuntime(source).children as Node[];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function expectChildren(source: Node, expected: readonly Node[]): void {
|
|
42
|
+
const actual = getChildren(source);
|
|
43
|
+
expect(actual).toHaveLength(expected.length);
|
|
44
|
+
for (let i = 0; i < expected.length; i++) expect(actual[i]).toBe(expected[i]);
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
function attachAll() {
|
|
42
48
|
addNodeChild(container, childA);
|
|
43
49
|
addNodeChild(container, childB);
|
|
@@ -91,7 +97,7 @@ describe('applyNodeOrderList', () => {
|
|
|
91
97
|
|
|
92
98
|
applyNodeOrderList(container, list);
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
expectChildren(container, [childC, childB, childA]);
|
|
95
101
|
});
|
|
96
102
|
|
|
97
103
|
it('never moves a child the list does not name', () => {
|
|
@@ -107,7 +113,7 @@ describe('applyNodeOrderList', () => {
|
|
|
107
113
|
|
|
108
114
|
// The members swap across the slots they held (0 and 2); the foreign child keeps slot 1, so it
|
|
109
115
|
// still sits between them rather than being compacted to one side.
|
|
110
|
-
|
|
116
|
+
expectChildren(container, [childB, foreign, childA]);
|
|
111
117
|
});
|
|
112
118
|
|
|
113
119
|
it('ignores entries that are not children of the target', () => {
|
|
@@ -121,7 +127,7 @@ describe('applyNodeOrderList', () => {
|
|
|
121
127
|
|
|
122
128
|
applyNodeOrderList(container, list);
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
expectChildren(container, [childB, childA]);
|
|
125
131
|
});
|
|
126
132
|
|
|
127
133
|
it('ignores entries attached to a different parent', () => {
|
|
@@ -136,8 +142,8 @@ describe('applyNodeOrderList', () => {
|
|
|
136
142
|
|
|
137
143
|
applyNodeOrderList(container, list);
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
expectChildren(container, [childB, childA]);
|
|
146
|
+
expectChildren(other, [childC]);
|
|
141
147
|
});
|
|
142
148
|
|
|
143
149
|
it('is a no-op once every member has been removed', () => {
|
|
@@ -152,7 +158,7 @@ describe('applyNodeOrderList', () => {
|
|
|
152
158
|
|
|
153
159
|
applyNodeOrderList(container, list);
|
|
154
160
|
|
|
155
|
-
|
|
161
|
+
expectChildren(container, [foreign]);
|
|
156
162
|
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
157
163
|
});
|
|
158
164
|
|
|
@@ -166,7 +172,7 @@ describe('applyNodeOrderList', () => {
|
|
|
166
172
|
|
|
167
173
|
applyNodeOrderList(container, list);
|
|
168
174
|
|
|
169
|
-
|
|
175
|
+
expectChildren(container, [childC, childA]);
|
|
170
176
|
});
|
|
171
177
|
|
|
172
178
|
it('breaks equal sort keys by the order the entries were added', () => {
|
|
@@ -180,7 +186,7 @@ describe('applyNodeOrderList', () => {
|
|
|
180
186
|
|
|
181
187
|
applyNodeOrderList(container, list);
|
|
182
188
|
|
|
183
|
-
|
|
189
|
+
expectChildren(container, [childA, childB, childC]);
|
|
184
190
|
});
|
|
185
191
|
|
|
186
192
|
it('resolves a node entered twice to its last entry', () => {
|
|
@@ -193,7 +199,7 @@ describe('applyNodeOrderList', () => {
|
|
|
193
199
|
|
|
194
200
|
applyNodeOrderList(container, list);
|
|
195
201
|
|
|
196
|
-
|
|
202
|
+
expectChildren(container, [childB, childA]);
|
|
197
203
|
});
|
|
198
204
|
|
|
199
205
|
it('is idempotent', () => {
|
|
@@ -207,7 +213,7 @@ describe('applyNodeOrderList', () => {
|
|
|
207
213
|
|
|
208
214
|
applyNodeOrderList(container, list);
|
|
209
215
|
|
|
210
|
-
|
|
216
|
+
expectChildren(container, [childB, childA]);
|
|
211
217
|
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
212
218
|
});
|
|
213
219
|
|
|
@@ -263,9 +269,31 @@ describe('applyNodeOrderList', () => {
|
|
|
263
269
|
|
|
264
270
|
applyNodeOrderList(container, createNodeOrderList());
|
|
265
271
|
|
|
266
|
-
|
|
272
|
+
expectChildren(container, [childA, childB]);
|
|
267
273
|
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
268
274
|
});
|
|
275
|
+
|
|
276
|
+
it('leaves a childless target alone when the list is nonempty', () => {
|
|
277
|
+
const list = createNodeOrderList();
|
|
278
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
279
|
+
|
|
280
|
+
expect(() => applyNodeOrderList(container, list)).not.toThrow();
|
|
281
|
+
expect(getNodeRuntime(container).children).toBeNull();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('does not apply retained entries outside the valid window', () => {
|
|
285
|
+
addNodeChild(container, childC);
|
|
286
|
+
addNodeChild(container, childB);
|
|
287
|
+
const list = createNodeOrderList();
|
|
288
|
+
addNodeOrderListEntry(list, childC, 10);
|
|
289
|
+
addNodeOrderListEntry(list, childB, 0);
|
|
290
|
+
clearNodeOrderList(list);
|
|
291
|
+
addNodeOrderListEntry(list, childC, 10);
|
|
292
|
+
|
|
293
|
+
applyNodeOrderList(container, list);
|
|
294
|
+
|
|
295
|
+
expectChildren(container, [childC, childB]);
|
|
296
|
+
});
|
|
269
297
|
});
|
|
270
298
|
|
|
271
299
|
describe('clearNodeOrderList', () => {
|
|
@@ -433,7 +461,7 @@ describe('removeNodeOrderListEntry', () => {
|
|
|
433
461
|
|
|
434
462
|
// A keeps slot 0 — only its entry was removed, not the child. C was entered before B and stays
|
|
435
463
|
// before it; a swap-with-last removal would have put B first and left the children untouched.
|
|
436
|
-
|
|
464
|
+
expectChildren(container, [childA, childC, childB]);
|
|
437
465
|
});
|
|
438
466
|
});
|
|
439
467
|
|
|
@@ -467,7 +495,7 @@ describe('setNodeOrderListEntryAbove', () => {
|
|
|
467
495
|
setNodeOrderListEntryAbove(list, childA, childC);
|
|
468
496
|
applyNodeOrderList(container, list);
|
|
469
497
|
|
|
470
|
-
|
|
498
|
+
expectChildren(container, [childB, childC, childA]);
|
|
471
499
|
});
|
|
472
500
|
|
|
473
501
|
it('places between neighbours whose keys leave no gap', () => {
|
|
@@ -477,11 +505,20 @@ describe('setNodeOrderListEntryAbove', () => {
|
|
|
477
505
|
addNodeOrderListEntry(list, childC, 6);
|
|
478
506
|
|
|
479
507
|
setNodeOrderListEntryAbove(list, childA, childB);
|
|
508
|
+
|
|
509
|
+
expect(list.entryCount).toBe(3);
|
|
510
|
+
expect(list.nodes).toHaveLength(3);
|
|
511
|
+
expect(list.sortKeys).toHaveLength(3);
|
|
512
|
+
expect(list.nodes[0]).toBe(childB);
|
|
513
|
+
expect(list.nodes[1]).toBe(childA);
|
|
514
|
+
expect(list.nodes[2]).toBe(childC);
|
|
515
|
+
expect(list.sortKeys.slice(0, 3)).toEqual([5, 5, 6]);
|
|
516
|
+
|
|
480
517
|
applyNodeOrderList(container, list);
|
|
481
518
|
|
|
482
519
|
// No midpoint exists between 5 and 6, so bisection could not express this; the equal-key plus
|
|
483
520
|
// entry-position rule can.
|
|
484
|
-
|
|
521
|
+
expectChildren(container, [childB, childA, childC]);
|
|
485
522
|
});
|
|
486
523
|
|
|
487
524
|
it('moves a node that is already entered rather than duplicating it', () => {
|
|
@@ -503,6 +540,16 @@ describe('setNodeOrderListEntryAbove', () => {
|
|
|
503
540
|
expect(list.entryCount).toBe(0);
|
|
504
541
|
});
|
|
505
542
|
|
|
543
|
+
it('keeps an existing node entry when the target has no entry', () => {
|
|
544
|
+
const list = createNodeOrderList();
|
|
545
|
+
addNodeOrderListEntry(list, childA, 4);
|
|
546
|
+
|
|
547
|
+
setNodeOrderListEntryAbove(list, childA, childC);
|
|
548
|
+
|
|
549
|
+
expect(list.entryCount).toBe(1);
|
|
550
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(4);
|
|
551
|
+
});
|
|
552
|
+
|
|
506
553
|
it('does nothing when the node is its own target', () => {
|
|
507
554
|
const list = createNodeOrderList();
|
|
508
555
|
addNodeOrderListEntry(list, childA, 4);
|
|
@@ -523,7 +570,7 @@ describe('setNodeOrderListEntryBelow', () => {
|
|
|
523
570
|
setNodeOrderListEntryBelow(list, childA, childC);
|
|
524
571
|
applyNodeOrderList(container, list);
|
|
525
572
|
|
|
526
|
-
|
|
573
|
+
expectChildren(container, [childB, childA, childC]);
|
|
527
574
|
});
|
|
528
575
|
|
|
529
576
|
it('places between neighbours whose keys leave no gap', () => {
|
|
@@ -535,7 +582,7 @@ describe('setNodeOrderListEntryBelow', () => {
|
|
|
535
582
|
setNodeOrderListEntryBelow(list, childA, childC);
|
|
536
583
|
applyNodeOrderList(container, list);
|
|
537
584
|
|
|
538
|
-
|
|
585
|
+
expectChildren(container, [childB, childA, childC]);
|
|
539
586
|
});
|
|
540
587
|
|
|
541
588
|
it('does nothing when the target has no entry', () => {
|
|
@@ -545,6 +592,16 @@ describe('setNodeOrderListEntryBelow', () => {
|
|
|
545
592
|
|
|
546
593
|
expect(list.entryCount).toBe(0);
|
|
547
594
|
});
|
|
595
|
+
|
|
596
|
+
it('keeps an existing node entry when the target has no entry', () => {
|
|
597
|
+
const list = createNodeOrderList();
|
|
598
|
+
addNodeOrderListEntry(list, childA, 4);
|
|
599
|
+
|
|
600
|
+
setNodeOrderListEntryBelow(list, childA, childC);
|
|
601
|
+
|
|
602
|
+
expect(list.entryCount).toBe(1);
|
|
603
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(4);
|
|
604
|
+
});
|
|
548
605
|
});
|
|
549
606
|
|
|
550
607
|
describe('setNodeOrderListFromNodeChildren', () => {
|
|
@@ -568,7 +625,7 @@ describe('setNodeOrderListFromNodeChildren', () => {
|
|
|
568
625
|
|
|
569
626
|
applyNodeOrderList(container, list);
|
|
570
627
|
|
|
571
|
-
|
|
628
|
+
expectChildren(container, [childA, childB, childC]);
|
|
572
629
|
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
573
630
|
});
|
|
574
631
|
|
|
@@ -603,7 +660,7 @@ describe('swapNodeOrderListEntries', () => {
|
|
|
603
660
|
swapNodeOrderListEntries(list, childA, childC);
|
|
604
661
|
applyNodeOrderList(container, list);
|
|
605
662
|
|
|
606
|
-
|
|
663
|
+
expectChildren(container, [childC, childB, childA]);
|
|
607
664
|
});
|
|
608
665
|
|
|
609
666
|
it('does nothing unless both nodes are entered', () => {
|
|
@@ -614,4 +671,13 @@ describe('swapNodeOrderListEntries', () => {
|
|
|
614
671
|
|
|
615
672
|
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(1);
|
|
616
673
|
});
|
|
674
|
+
|
|
675
|
+
it('does nothing when the first node has no entry', () => {
|
|
676
|
+
const list = createNodeOrderList();
|
|
677
|
+
addNodeOrderListEntry(list, childB, 2);
|
|
678
|
+
|
|
679
|
+
swapNodeOrderListEntries(list, childA, childB);
|
|
680
|
+
|
|
681
|
+
expect(getNodeOrderListEntrySortKey(list, childB)).toBe(2);
|
|
682
|
+
});
|
|
617
683
|
});
|
|
@@ -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;
|