@flighthq/node 0.3.1-next.847.2e6f8bf → 0.3.1-next.887.9f59a84
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/hierarchy.d.ts +3 -3
- package/dist/hierarchy.d.ts.map +1 -1
- package/dist/hierarchy.js +20 -20
- package/dist/hierarchy.js.map +1 -1
- package/dist/nodeOrderList.js +2 -2
- package/dist/nodeOrderList.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 +7 -7
- package/src/boundsRectangle.test.ts +228 -5
- package/src/hierarchy.test.ts +67 -3
- package/src/nodeOrderList.test.ts +75 -18
- package/src/nodeTransform2d.test.ts +23 -0
- package/src/revision.test.ts +26 -0
- package/src/stageFit.test.ts +56 -1
- package/src/traversal.test.ts +6 -0
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
initTransform2DRuntimeTrait,
|
|
14
14
|
initTransform2DTrait,
|
|
15
15
|
invalidateNodeLocalTransform,
|
|
16
|
+
removeNodeChild,
|
|
17
|
+
setNodeEnabled,
|
|
16
18
|
} from '@flighthq/node/contract';
|
|
17
19
|
import type {
|
|
18
20
|
HasBoundsRectangle,
|
|
@@ -120,9 +122,10 @@ describe('computeNodeBoundsRectangle', () => {
|
|
|
120
122
|
it('should compute bounds relative to nested child', () => {
|
|
121
123
|
const out = createRectangle();
|
|
122
124
|
computeNodeBoundsRectangle(out, root, grandChild);
|
|
123
|
-
expect(out.
|
|
124
|
-
expect(out.
|
|
125
|
-
|
|
125
|
+
expect(out.x).toBeCloseTo(-100);
|
|
126
|
+
expect(out.y).toBeCloseTo(-100);
|
|
127
|
+
expect(out.width).toBeCloseTo(205);
|
|
128
|
+
expect(out.height).toBeCloseTo(205);
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
it('should account for scaling in parent transforms', () => {
|
|
@@ -187,6 +190,20 @@ describe('computeNodeBoundsRectangle', () => {
|
|
|
187
190
|
computeNodeBoundsRectangle(out, child, root);
|
|
188
191
|
expect(equalsRectangle(out, getNodeWorldBoundsRectangle(child))).toBe(true);
|
|
189
192
|
});
|
|
193
|
+
|
|
194
|
+
it('removes a detached target transform from target-relative bounds', () => {
|
|
195
|
+
root.x = 40;
|
|
196
|
+
root.y = -25;
|
|
197
|
+
invalidateNodeLocalTransform(root);
|
|
198
|
+
const out = createRectangle();
|
|
199
|
+
|
|
200
|
+
computeNodeBoundsRectangle(out, child, root);
|
|
201
|
+
|
|
202
|
+
expect(out.x).toBeCloseTo(105);
|
|
203
|
+
expect(out.y).toBeCloseTo(105);
|
|
204
|
+
expect(out.width).toBeCloseTo(100);
|
|
205
|
+
expect(out.height).toBeCloseTo(100);
|
|
206
|
+
});
|
|
190
207
|
});
|
|
191
208
|
|
|
192
209
|
describe('computeNodeRootLocalBoundsRectangle', () => {
|
|
@@ -573,8 +590,71 @@ describe('getNodeWorldBoundsRectangle', () => {
|
|
|
573
590
|
invalidateNodeLocalTransform(child);
|
|
574
591
|
|
|
575
592
|
const bounds = getNodeWorldBoundsRectangle(parent);
|
|
576
|
-
expect(bounds
|
|
577
|
-
|
|
593
|
+
expect(bounds).toMatchObject({ x: 0, y: 0, width: 300, height: 200 });
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('refreshes cached aggregate bounds when a child moves', () => {
|
|
597
|
+
const parent = createTestNode();
|
|
598
|
+
const child = createTestNode();
|
|
599
|
+
addNodeChild(parent, child);
|
|
600
|
+
setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
|
|
601
|
+
setRectangle(getNodeLocalBoundsRectangle(child) as Rectangle, 0, 0, 20, 20);
|
|
602
|
+
child.x = 30;
|
|
603
|
+
invalidateNodeLocalTransform(child);
|
|
604
|
+
expect(getNodeWorldBoundsRectangle(parent)).toMatchObject({ x: 0, y: 0, width: 50, height: 20 });
|
|
605
|
+
|
|
606
|
+
child.x = 80;
|
|
607
|
+
invalidateNodeLocalTransform(child);
|
|
608
|
+
|
|
609
|
+
expect(getNodeWorldBoundsRectangle(parent)).toMatchObject({ x: 0, y: 0, width: 100, height: 20 });
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('consults a descendant kind-owned bounds validity hook after the aggregate is cached', () => {
|
|
613
|
+
const parent = createTestNode();
|
|
614
|
+
const child = createTestNode();
|
|
615
|
+
const runtime = getEntityRuntime(child);
|
|
616
|
+
let input = { width: 20 };
|
|
617
|
+
let stampedInput = input;
|
|
618
|
+
runtime.computeLocalBoundsRectangle = (out: Rectangle) => {
|
|
619
|
+
setRectangle(out, 100, 0, input.width, 10);
|
|
620
|
+
stampedInput = input;
|
|
621
|
+
};
|
|
622
|
+
runtime.isLocalBoundsRectangleValid = () => stampedInput === input;
|
|
623
|
+
addNodeChild(parent, child);
|
|
624
|
+
setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
|
|
625
|
+
expect(getNodeWorldBoundsRectangle(parent).width).toBe(120);
|
|
626
|
+
|
|
627
|
+
input = { width: 50 };
|
|
628
|
+
|
|
629
|
+
expect(getNodeWorldBoundsRectangle(parent).width).toBe(150);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('refreshes cached aggregate bounds when a child is removed', () => {
|
|
633
|
+
const parent = createTestNode();
|
|
634
|
+
const child = createTestNode();
|
|
635
|
+
addNodeChild(parent, child);
|
|
636
|
+
setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
|
|
637
|
+
setRectangle(getNodeLocalBoundsRectangle(child) as Rectangle, 100, 0, 200, 200);
|
|
638
|
+
expect(getNodeWorldBoundsRectangle(parent).width).toBe(300);
|
|
639
|
+
|
|
640
|
+
removeNodeChild(parent, child);
|
|
641
|
+
|
|
642
|
+
expect(getNodeWorldBoundsRectangle(parent)).toMatchObject({ x: 0, y: 0, width: 10, height: 10 });
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('refreshes cached aggregate bounds when a child is disabled or re-enabled', () => {
|
|
646
|
+
const parent = createTestNode();
|
|
647
|
+
const child = createTestNode();
|
|
648
|
+
addNodeChild(parent, child);
|
|
649
|
+
setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
|
|
650
|
+
setRectangle(getNodeLocalBoundsRectangle(child) as Rectangle, 100, 0, 200, 200);
|
|
651
|
+
expect(getNodeWorldBoundsRectangle(parent).width).toBe(300);
|
|
652
|
+
|
|
653
|
+
setNodeEnabled(child, false);
|
|
654
|
+
expect(getNodeWorldBoundsRectangle(parent).width).toBe(10);
|
|
655
|
+
|
|
656
|
+
setNodeEnabled(child, true);
|
|
657
|
+
expect(getNodeWorldBoundsRectangle(parent).width).toBe(300);
|
|
578
658
|
});
|
|
579
659
|
|
|
580
660
|
it('ignores an enabled child with zero-area world bounds', () => {
|
|
@@ -609,6 +689,74 @@ describe('setNodeHeight', () => {
|
|
|
609
689
|
setNodeHeight(node, 100);
|
|
610
690
|
expect(node.scaleY).toBe(0);
|
|
611
691
|
});
|
|
692
|
+
|
|
693
|
+
it('keeps a finite scale when the current height is zero', () => {
|
|
694
|
+
const node = createTestNode();
|
|
695
|
+
setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 10, 0);
|
|
696
|
+
|
|
697
|
+
setNodeHeight(node, 100);
|
|
698
|
+
|
|
699
|
+
expect(node.scaleY).toBe(1);
|
|
700
|
+
expect(Number.isFinite(node.scaleY)).toBe(true);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
for (const [name, transform] of sizeRoundTripTransforms) {
|
|
704
|
+
it(`round-trips parent-space height through ${name}`, () => {
|
|
705
|
+
const parent = createTestNode();
|
|
706
|
+
const node = createTestNode();
|
|
707
|
+
addNodeChild(parent, node);
|
|
708
|
+
setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 10, 20);
|
|
709
|
+
Object.assign(node, transform);
|
|
710
|
+
invalidateNodeLocalTransform(node);
|
|
711
|
+
const sign = Math.sign(node.scaleY);
|
|
712
|
+
|
|
713
|
+
setNodeHeight(node, 100);
|
|
714
|
+
|
|
715
|
+
expect(getNodeHeight(node)).toBeCloseTo(100, 10);
|
|
716
|
+
expect(Math.sign(node.scaleY)).toBe(sign);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
it('declines an unsatisfiable height below the fixed x-axis contribution', () => {
|
|
721
|
+
const parent = createTestNode();
|
|
722
|
+
const node = createTestNode();
|
|
723
|
+
addNodeChild(parent, node);
|
|
724
|
+
setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 10, 20);
|
|
725
|
+
node.rotation = 30;
|
|
726
|
+
invalidateNodeLocalTransform(node);
|
|
727
|
+
const scaleY = node.scaleY;
|
|
728
|
+
|
|
729
|
+
setNodeHeight(node, 4);
|
|
730
|
+
|
|
731
|
+
expect(node.scaleY).toBe(scaleY);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('does not mutate at a singular rotation and still solves near it', () => {
|
|
735
|
+
const singularParent = createTestNode();
|
|
736
|
+
const singular = createTestNode();
|
|
737
|
+
addNodeChild(singularParent, singular);
|
|
738
|
+
setRectangle(getNodeLocalBoundsRectangle(singular) as Rectangle, 0, 0, 10, 20);
|
|
739
|
+
singular.rotation = 90;
|
|
740
|
+
singular.scaleY = 2;
|
|
741
|
+
invalidateNodeLocalTransform(singular);
|
|
742
|
+
|
|
743
|
+
setNodeHeight(singular, 50);
|
|
744
|
+
|
|
745
|
+
expect(singular.scaleY).toBe(2);
|
|
746
|
+
expect(Number.isFinite(singular.scaleY)).toBe(true);
|
|
747
|
+
|
|
748
|
+
const nearParent = createTestNode();
|
|
749
|
+
const near = createTestNode();
|
|
750
|
+
addNodeChild(nearParent, near);
|
|
751
|
+
setRectangle(getNodeLocalBoundsRectangle(near) as Rectangle, 0, 0, 10, 20);
|
|
752
|
+
near.rotation = 89.999;
|
|
753
|
+
invalidateNodeLocalTransform(near);
|
|
754
|
+
|
|
755
|
+
setNodeHeight(near, 50);
|
|
756
|
+
|
|
757
|
+
expect(Number.isFinite(near.scaleY)).toBe(true);
|
|
758
|
+
expect(getNodeHeight(near)).toBeCloseTo(50, 8);
|
|
759
|
+
});
|
|
612
760
|
});
|
|
613
761
|
|
|
614
762
|
describe('setNodeWidth', () => {
|
|
@@ -628,6 +776,74 @@ describe('setNodeWidth', () => {
|
|
|
628
776
|
setNodeWidth(node, 100);
|
|
629
777
|
expect(node.scaleX).toBe(0);
|
|
630
778
|
});
|
|
779
|
+
|
|
780
|
+
it('keeps a finite scale when the current width is zero', () => {
|
|
781
|
+
const node = createTestNode();
|
|
782
|
+
setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 0, 10);
|
|
783
|
+
|
|
784
|
+
setNodeWidth(node, 100);
|
|
785
|
+
|
|
786
|
+
expect(node.scaleX).toBe(1);
|
|
787
|
+
expect(Number.isFinite(node.scaleX)).toBe(true);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
for (const [name, transform] of sizeRoundTripTransforms) {
|
|
791
|
+
it(`round-trips parent-space width through ${name}`, () => {
|
|
792
|
+
const parent = createTestNode();
|
|
793
|
+
const node = createTestNode();
|
|
794
|
+
addNodeChild(parent, node);
|
|
795
|
+
setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 10, 20);
|
|
796
|
+
Object.assign(node, transform);
|
|
797
|
+
invalidateNodeLocalTransform(node);
|
|
798
|
+
const sign = Math.sign(node.scaleX);
|
|
799
|
+
|
|
800
|
+
setNodeWidth(node, 100);
|
|
801
|
+
|
|
802
|
+
expect(getNodeWidth(node)).toBeCloseTo(100, 10);
|
|
803
|
+
expect(Math.sign(node.scaleX)).toBe(sign);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
it('declines an unsatisfiable width below the fixed y-axis contribution', () => {
|
|
808
|
+
const parent = createTestNode();
|
|
809
|
+
const node = createTestNode();
|
|
810
|
+
addNodeChild(parent, node);
|
|
811
|
+
setRectangle(getNodeLocalBoundsRectangle(node) as Rectangle, 0, 0, 10, 20);
|
|
812
|
+
node.rotation = 30;
|
|
813
|
+
invalidateNodeLocalTransform(node);
|
|
814
|
+
const scaleX = node.scaleX;
|
|
815
|
+
|
|
816
|
+
setNodeWidth(node, 5);
|
|
817
|
+
|
|
818
|
+
expect(node.scaleX).toBe(scaleX);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('does not mutate at a singular rotation and still solves near it', () => {
|
|
822
|
+
const singularParent = createTestNode();
|
|
823
|
+
const singular = createTestNode();
|
|
824
|
+
addNodeChild(singularParent, singular);
|
|
825
|
+
setRectangle(getNodeLocalBoundsRectangle(singular) as Rectangle, 0, 0, 10, 20);
|
|
826
|
+
singular.rotation = 90;
|
|
827
|
+
singular.scaleX = 2;
|
|
828
|
+
invalidateNodeLocalTransform(singular);
|
|
829
|
+
|
|
830
|
+
setNodeWidth(singular, 50);
|
|
831
|
+
|
|
832
|
+
expect(singular.scaleX).toBe(2);
|
|
833
|
+
expect(Number.isFinite(singular.scaleX)).toBe(true);
|
|
834
|
+
|
|
835
|
+
const nearParent = createTestNode();
|
|
836
|
+
const near = createTestNode();
|
|
837
|
+
addNodeChild(nearParent, near);
|
|
838
|
+
setRectangle(getNodeLocalBoundsRectangle(near) as Rectangle, 0, 0, 10, 20);
|
|
839
|
+
near.rotation = 89.999;
|
|
840
|
+
invalidateNodeLocalTransform(near);
|
|
841
|
+
|
|
842
|
+
setNodeWidth(near, 50);
|
|
843
|
+
|
|
844
|
+
expect(Number.isFinite(near.scaleX)).toBe(true);
|
|
845
|
+
expect(getNodeWidth(near)).toBeCloseTo(50, 8);
|
|
846
|
+
});
|
|
631
847
|
});
|
|
632
848
|
|
|
633
849
|
function cloneAndInvalidateRect(rect: Rectangle): Rectangle {
|
|
@@ -642,4 +858,11 @@ function invalidateRect(rect: Rectangle | null): void {
|
|
|
642
858
|
|
|
643
859
|
type TestNode = Node<HasTransform2D & HasBoundsRectangle> & HasTransform2D & HasBoundsRectangle;
|
|
644
860
|
|
|
861
|
+
const sizeRoundTripTransforms = [
|
|
862
|
+
['rotation', { rotation: 30, scaleX: 1, scaleY: 1, skewX: 0, skewY: 0 }],
|
|
863
|
+
['skew and non-uniform scale', { rotation: 10, scaleX: 0.7, scaleY: 2.3, skewX: 25, skewY: -15 }],
|
|
864
|
+
['mirrored x scale', { rotation: -25, scaleX: -1.4, scaleY: 0.6, skewX: -10, skewY: 20 }],
|
|
865
|
+
['mirrored y scale', { rotation: 40, scaleX: 1.8, scaleY: -1.2, skewX: 12, skewY: -18 }],
|
|
866
|
+
] as const;
|
|
867
|
+
|
|
645
868
|
const TestKind = 'Test';
|
package/src/hierarchy.test.ts
CHANGED
|
@@ -67,6 +67,15 @@ describe('addNodeChild', () => {
|
|
|
67
67
|
expect(() => addNodeChild(container, container)).toThrow(TypeError);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
it('throws rather than creating a cycle', () => {
|
|
71
|
+
addNodeChild(container, childA);
|
|
72
|
+
addNodeChild(childA, childB);
|
|
73
|
+
|
|
74
|
+
expect(() => addNodeChild(childB, container)).toThrow(TypeError);
|
|
75
|
+
expect(getNodeParent(container)).toBeNull();
|
|
76
|
+
expect(getNodeRoot(childB)).toBe(container);
|
|
77
|
+
});
|
|
78
|
+
|
|
70
79
|
it('rejects a child disallowed by the parent runtime', () => {
|
|
71
80
|
getEntityRuntime(container).canAddChild = () => false;
|
|
72
81
|
|
|
@@ -771,27 +780,81 @@ describe('reparentNode', () => {
|
|
|
771
780
|
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
772
781
|
});
|
|
773
782
|
|
|
774
|
-
it('preserves
|
|
783
|
+
it('preserves shear induced by rotation across non-uniformly scaled parents', () => {
|
|
784
|
+
const parentA = createTransformNode();
|
|
785
|
+
parentA.rotation = -15;
|
|
786
|
+
parentA.scaleX = 0.75;
|
|
787
|
+
parentA.scaleY = 2.5;
|
|
788
|
+
invalidateNodeLocalTransform(parentA);
|
|
789
|
+
|
|
790
|
+
const child = createTransformNode();
|
|
791
|
+
child.x = 10;
|
|
792
|
+
child.y = 5;
|
|
793
|
+
child.rotation = 37;
|
|
794
|
+
child.scaleX = 1.2;
|
|
795
|
+
child.scaleY = 0.9;
|
|
796
|
+
invalidateNodeLocalTransform(child);
|
|
797
|
+
addNodeChild(parentA, child);
|
|
798
|
+
|
|
799
|
+
expect(child.skewX).toBe(0);
|
|
800
|
+
expect(child.skewY).toBe(0);
|
|
801
|
+
const before = cloneMatrix(getNodeWorldMatrix(child));
|
|
802
|
+
|
|
803
|
+
const parentB = createTransformNode();
|
|
804
|
+
parentB.rotation = 22;
|
|
805
|
+
parentB.scaleX = 2.4;
|
|
806
|
+
parentB.scaleY = 0.6;
|
|
807
|
+
invalidateNodeLocalTransform(parentB);
|
|
808
|
+
|
|
809
|
+
reparentNode(child, parentB);
|
|
810
|
+
const after = getNodeWorldMatrix(child);
|
|
811
|
+
|
|
812
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
813
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
814
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
815
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
816
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
817
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('preserves a skewed world transform across asymmetric parents', () => {
|
|
775
821
|
const parentA = createTransformNode();
|
|
776
822
|
parentA.x = 50;
|
|
777
823
|
parentA.rotation = 45;
|
|
824
|
+
parentA.scaleX = 2;
|
|
825
|
+
parentA.scaleY = 3;
|
|
778
826
|
invalidateNodeLocalTransform(parentA);
|
|
779
827
|
|
|
780
828
|
const child = createTransformNode();
|
|
781
829
|
child.x = 10;
|
|
830
|
+
child.y = 5;
|
|
831
|
+
child.rotation = 12;
|
|
832
|
+
child.scaleX = 1.4;
|
|
833
|
+
child.scaleY = 0.8;
|
|
782
834
|
child.skewX = 15;
|
|
783
835
|
child.skewY = 20;
|
|
784
836
|
invalidateNodeLocalTransform(child);
|
|
785
837
|
addNodeChild(parentA, child);
|
|
786
838
|
|
|
839
|
+
const before = cloneMatrix(getNodeWorldMatrix(child));
|
|
840
|
+
|
|
787
841
|
const parentB = createTransformNode();
|
|
788
842
|
parentB.x = 100;
|
|
843
|
+
parentB.y = -20;
|
|
844
|
+
parentB.rotation = -10;
|
|
845
|
+
parentB.scaleX = 0.7;
|
|
846
|
+
parentB.scaleY = 1.6;
|
|
789
847
|
invalidateNodeLocalTransform(parentB);
|
|
790
848
|
|
|
791
849
|
reparentNode(child, parentB);
|
|
850
|
+
const after = getNodeWorldMatrix(child);
|
|
792
851
|
|
|
793
|
-
expect(
|
|
794
|
-
expect(
|
|
852
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
853
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
854
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
855
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
856
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
857
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
795
858
|
});
|
|
796
859
|
|
|
797
860
|
it('preserves world position with pivot', () => {
|
|
@@ -873,6 +936,7 @@ describe('reparentNode', () => {
|
|
|
873
936
|
reparentNode(child, parentB);
|
|
874
937
|
const after = getNodeWorldMatrix(child);
|
|
875
938
|
|
|
939
|
+
expect(child.scaleY).toBeLessThan(0);
|
|
876
940
|
expect(after.a).toBeCloseTo(before.a, 10);
|
|
877
941
|
expect(after.b).toBeCloseTo(before.b, 10);
|
|
878
942
|
expect(after.c).toBeCloseTo(before.c, 10);
|
|
@@ -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', () => {
|
|
@@ -304,6 +304,29 @@ describe('setNodeLocalMatrix', () => {
|
|
|
304
304
|
expect(m.ty).toBeCloseTo(20);
|
|
305
305
|
});
|
|
306
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
|
+
|
|
307
330
|
it('resets pivot and absorbs its offset into translation', () => {
|
|
308
331
|
node.pivotX = 5;
|
|
309
332
|
node.pivotY = 5;
|