@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
|
@@ -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,
|
|
@@ -42,8 +44,10 @@ import { initBoundsRectangleRuntimeTrait, initBoundsRectangleTrait } from './has
|
|
|
42
44
|
|
|
43
45
|
function getEntityRuntime(
|
|
44
46
|
source: TestNode,
|
|
45
|
-
): NodeRuntime<HasBoundsRectangle & HasTransform2D> & HasBoundsRectangleRuntime {
|
|
46
|
-
return _getRuntime(source) as NodeRuntime<HasBoundsRectangle & HasTransform2D> &
|
|
47
|
+
): NodeRuntime<HasBoundsRectangle & HasTransform2D> & HasBoundsRectangleRuntime & HasTransform2DRuntime {
|
|
48
|
+
return _getRuntime(source) as NodeRuntime<HasBoundsRectangle & HasTransform2D> &
|
|
49
|
+
HasBoundsRectangleRuntime &
|
|
50
|
+
HasTransform2DRuntime;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
function createTestNode(): TestNode {
|
|
@@ -90,6 +94,14 @@ describe('computeNodeBoundsRectangle', () => {
|
|
|
90
94
|
expect(equalsRectangle(out, { x: 5, y: 5, width: 100, height: 100 })).toBe(true);
|
|
91
95
|
});
|
|
92
96
|
|
|
97
|
+
it('defaults a null target coordinate space to the source', () => {
|
|
98
|
+
const out = createRectangle();
|
|
99
|
+
|
|
100
|
+
computeNodeBoundsRectangle(out, child, null);
|
|
101
|
+
|
|
102
|
+
expect(equalsRectangle(out, { x: 5, y: 5, width: 100, height: 100 })).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
93
105
|
it('should compute bounds relative to parent', () => {
|
|
94
106
|
const out = createRectangle();
|
|
95
107
|
computeNodeBoundsRectangle(out, child, root);
|
|
@@ -99,12 +111,21 @@ describe('computeNodeBoundsRectangle', () => {
|
|
|
99
111
|
expect(out.height).toBeCloseTo(100);
|
|
100
112
|
});
|
|
101
113
|
|
|
114
|
+
it('uses parent-space bounds directly when the parent is nested', () => {
|
|
115
|
+
const out = createRectangle();
|
|
116
|
+
|
|
117
|
+
computeNodeBoundsRectangle(out, grandChild, child);
|
|
118
|
+
|
|
119
|
+
expect(equalsRectangle(out, getNodeParentBoundsRectangle(grandChild))).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
102
122
|
it('should compute bounds relative to nested child', () => {
|
|
103
123
|
const out = createRectangle();
|
|
104
124
|
computeNodeBoundsRectangle(out, root, grandChild);
|
|
105
|
-
expect(out.
|
|
106
|
-
expect(out.
|
|
107
|
-
|
|
125
|
+
expect(out.x).toBeCloseTo(-100);
|
|
126
|
+
expect(out.y).toBeCloseTo(-100);
|
|
127
|
+
expect(out.width).toBeCloseTo(205);
|
|
128
|
+
expect(out.height).toBeCloseTo(205);
|
|
108
129
|
});
|
|
109
130
|
|
|
110
131
|
it('should account for scaling in parent transforms', () => {
|
|
@@ -153,11 +174,36 @@ describe('computeNodeBoundsRectangle', () => {
|
|
|
153
174
|
expect(equalsRectangle(out, getNodeWorldBoundsRectangle(child))).toBe(true);
|
|
154
175
|
});
|
|
155
176
|
|
|
177
|
+
it('converts through world space for a nested unrelated target', () => {
|
|
178
|
+
const unrelatedRoot = createTestNode();
|
|
179
|
+
const unrelatedTarget = createTestNode();
|
|
180
|
+
addNodeChild(unrelatedRoot, unrelatedTarget);
|
|
181
|
+
const out = createRectangle();
|
|
182
|
+
|
|
183
|
+
computeNodeBoundsRectangle(out, grandChild, unrelatedTarget);
|
|
184
|
+
|
|
185
|
+
expect(equalsRectangle(out, getNodeWorldBoundsRectangle(grandChild))).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
156
188
|
it('should return world bounds if the target coordinate space is root', () => {
|
|
157
189
|
const out = createRectangle();
|
|
158
190
|
computeNodeBoundsRectangle(out, child, root);
|
|
159
191
|
expect(equalsRectangle(out, getNodeWorldBoundsRectangle(child))).toBe(true);
|
|
160
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
|
+
});
|
|
161
207
|
});
|
|
162
208
|
|
|
163
209
|
describe('computeNodeRootLocalBoundsRectangle', () => {
|
|
@@ -197,6 +243,39 @@ describe('computeNodeRootLocalBoundsRectangle', () => {
|
|
|
197
243
|
expect(out.width).toBeCloseTo(18);
|
|
198
244
|
expect(out.height).toBeCloseTo(24);
|
|
199
245
|
});
|
|
246
|
+
|
|
247
|
+
it('composes transforms at every descendant depth', () => {
|
|
248
|
+
const root = createTestNode();
|
|
249
|
+
const child = createTestNode();
|
|
250
|
+
const grandchild = createTestNode();
|
|
251
|
+
setRectangle(getNodeLocalBoundsRectangle(root), 0, 0, 1, 1);
|
|
252
|
+
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 1, 1);
|
|
253
|
+
setRectangle(getNodeLocalBoundsRectangle(grandchild), 0, 0, 2, 2);
|
|
254
|
+
child.x = 10;
|
|
255
|
+
grandchild.x = 20;
|
|
256
|
+
addNodeChild(root, child);
|
|
257
|
+
addNodeChild(child, grandchild);
|
|
258
|
+
|
|
259
|
+
const out = createRectangle();
|
|
260
|
+
computeNodeRootLocalBoundsRectangle(out, root);
|
|
261
|
+
|
|
262
|
+
expect(out).toMatchObject({ x: 0, y: 0, width: 32, height: 2 });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('excludes disabled descendants', () => {
|
|
266
|
+
const root = createTestNode();
|
|
267
|
+
const child = createTestNode();
|
|
268
|
+
setRectangle(getNodeLocalBoundsRectangle(root), 0, 0, 10, 10);
|
|
269
|
+
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 20, 20);
|
|
270
|
+
child.x = 100;
|
|
271
|
+
child.enabled = false;
|
|
272
|
+
addNodeChild(root, child);
|
|
273
|
+
|
|
274
|
+
const out = createRectangle();
|
|
275
|
+
computeNodeRootLocalBoundsRectangle(out, root);
|
|
276
|
+
|
|
277
|
+
expect(out).toMatchObject({ x: 0, y: 0, width: 10, height: 10 });
|
|
278
|
+
});
|
|
200
279
|
});
|
|
201
280
|
|
|
202
281
|
describe('ensureNodeLocalBoundsRectangle', () => {
|
|
@@ -316,6 +395,20 @@ describe('ensureNodeWorldBoundsRectangle', () => {
|
|
|
316
395
|
expect(runtime.worldBoundsRectangle).not.toBeNull();
|
|
317
396
|
});
|
|
318
397
|
|
|
398
|
+
it('recomputes when bounds are cached before the world matrix exists', () => {
|
|
399
|
+
const object = createTestNode();
|
|
400
|
+
const runtime = getEntityRuntime(object);
|
|
401
|
+
getNodeLocalBoundsRectangle(object);
|
|
402
|
+
runtime.worldBoundsRectangle = createRectangle(5, 5, 10, 10);
|
|
403
|
+
runtime.worldBoundsUsingLocalBoundsId = runtime.localBoundsId;
|
|
404
|
+
expect(runtime.worldMatrix).toBeNull();
|
|
405
|
+
|
|
406
|
+
ensureNodeWorldBoundsRectangle(object);
|
|
407
|
+
|
|
408
|
+
expect(runtime.worldMatrix).not.toBeNull();
|
|
409
|
+
expect(runtime.worldBoundsRectangle).toMatchObject({ x: 0, y: 0, width: 0, height: 0 });
|
|
410
|
+
});
|
|
411
|
+
|
|
319
412
|
it('should not recalculate if localBoundsId and worldTransformId are unchanged', () => {
|
|
320
413
|
const object = createTestNode();
|
|
321
414
|
const runtime = getEntityRuntime(object);
|
|
@@ -497,8 +590,85 @@ describe('getNodeWorldBoundsRectangle', () => {
|
|
|
497
590
|
invalidateNodeLocalTransform(child);
|
|
498
591
|
|
|
499
592
|
const bounds = getNodeWorldBoundsRectangle(parent);
|
|
500
|
-
expect(bounds
|
|
501
|
-
|
|
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);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('ignores an enabled child with zero-area world bounds', () => {
|
|
661
|
+
const parent = createTestNode();
|
|
662
|
+
const child = createTestNode();
|
|
663
|
+
addNodeChild(parent, child);
|
|
664
|
+
setRectangle(getNodeLocalBoundsRectangle(parent) as Rectangle, 0, 0, 10, 10);
|
|
665
|
+
setRectangle(getNodeLocalBoundsRectangle(child) as Rectangle, 0, 0, 0, 5);
|
|
666
|
+
child.x = 100;
|
|
667
|
+
invalidateNodeLocalTransform(child);
|
|
668
|
+
|
|
669
|
+
const bounds = getNodeWorldBoundsRectangle(parent);
|
|
670
|
+
|
|
671
|
+
expect(bounds).toMatchObject({ x: 0, y: 0, width: 10, height: 10 });
|
|
502
672
|
});
|
|
503
673
|
});
|
|
504
674
|
|
|
@@ -519,6 +689,74 @@ describe('setNodeHeight', () => {
|
|
|
519
689
|
setNodeHeight(node, 100);
|
|
520
690
|
expect(node.scaleY).toBe(0);
|
|
521
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
|
+
});
|
|
522
760
|
});
|
|
523
761
|
|
|
524
762
|
describe('setNodeWidth', () => {
|
|
@@ -538,6 +776,74 @@ describe('setNodeWidth', () => {
|
|
|
538
776
|
setNodeWidth(node, 100);
|
|
539
777
|
expect(node.scaleX).toBe(0);
|
|
540
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
|
+
});
|
|
541
847
|
});
|
|
542
848
|
|
|
543
849
|
function cloneAndInvalidateRect(rect: Rectangle): Rectangle {
|
|
@@ -552,4 +858,11 @@ function invalidateRect(rect: Rectangle | null): void {
|
|
|
552
858
|
|
|
553
859
|
type TestNode = Node<HasTransform2D & HasBoundsRectangle> & HasTransform2D & HasBoundsRectangle;
|
|
554
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
|
+
|
|
555
868
|
const TestKind = 'Test';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getEntityRuntime as getRawEntityRuntime } from '@flighthq/entity/contract';
|
|
2
|
+
import { setLogSink } from '@flighthq/log/contract';
|
|
3
|
+
import type { HasTransform2D, HasTransform2DRuntime, LogEntry, Node } from '@flighthq/types/contract';
|
|
4
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { areNodeGuardsEnabled, disableNodeGuards, enableNodeGuards } from './enableNodeGuards';
|
|
7
|
+
import { initTransform2DRuntimeTrait, initTransform2DTrait } from './hasTransform2d';
|
|
8
|
+
import { reparentNode } from './hierarchy';
|
|
9
|
+
import { createNode } from './node';
|
|
10
|
+
import { invalidateNodeLocalTransform } from './revision';
|
|
11
|
+
|
|
12
|
+
describe('areNodeGuardsEnabled', () => {
|
|
13
|
+
test('reports whether the guards are installed', () => {
|
|
14
|
+
expect(areNodeGuardsEnabled()).toBe(false);
|
|
15
|
+
enableNodeGuards();
|
|
16
|
+
expect(areNodeGuardsEnabled()).toBe(true);
|
|
17
|
+
disableNodeGuards();
|
|
18
|
+
expect(areNodeGuardsEnabled()).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('disableNodeGuards', () => {
|
|
23
|
+
test('leaves the core silent again once removed', () => {
|
|
24
|
+
enableNodeGuards();
|
|
25
|
+
disableNodeGuards();
|
|
26
|
+
expect(captureLog(() => reparentNode(transformNode(), collapsedParent('gone')))).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('enableNodeGuards', () => {
|
|
31
|
+
// `logOnce` keys on the parent name and has no reset, so each triggering test must use a DISTINCT
|
|
32
|
+
// name — a repeat would be deduplicated away and assert nothing while still looking green.
|
|
33
|
+
test('warns, naming both nodes, when a reparent is declined', () => {
|
|
34
|
+
enableNodeGuards();
|
|
35
|
+
const child = transformNode();
|
|
36
|
+
child.name = 'child';
|
|
37
|
+
const entries = captureLog(() => reparentNode(child, collapsedParent('collapsed-warns')));
|
|
38
|
+
expect(entries).toHaveLength(1);
|
|
39
|
+
expect(entries[0].data).toMatchObject({ child: 'child', newParent: 'collapsed-warns' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('stays silent when the reparent succeeds', () => {
|
|
43
|
+
enableNodeGuards();
|
|
44
|
+
const healthy = transformNode();
|
|
45
|
+
invalidateNodeLocalTransform(healthy);
|
|
46
|
+
expect(captureLog(() => reparentNode(transformNode(), healthy))).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
disableNodeGuards();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function captureLog(run: () => void): LogEntry[] {
|
|
55
|
+
const entries: LogEntry[] = [];
|
|
56
|
+
setLogSink((entry) => entries.push(entry));
|
|
57
|
+
try {
|
|
58
|
+
run();
|
|
59
|
+
} finally {
|
|
60
|
+
setLogSink(null);
|
|
61
|
+
}
|
|
62
|
+
return entries;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collapsedParent(name: string): Node<HasTransform2D> & HasTransform2D {
|
|
66
|
+
const parent = transformNode();
|
|
67
|
+
parent.name = name;
|
|
68
|
+
parent.scaleX = 0;
|
|
69
|
+
invalidateNodeLocalTransform(parent);
|
|
70
|
+
return parent;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function transformNode(): Node<HasTransform2D> & HasTransform2D {
|
|
74
|
+
const node = createNode('NodeGuardTest') as Node<HasTransform2D> & HasTransform2D;
|
|
75
|
+
initTransform2DTrait(node);
|
|
76
|
+
initTransform2DRuntimeTrait(getRawEntityRuntime(node) as HasTransform2DRuntime);
|
|
77
|
+
return node;
|
|
78
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { HasAppearance } from '@flighthq/types/contract';
|
|
1
|
+
import type { HasAppearance, HasAppearanceRuntime, NodeRuntime } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
|
-
import { initAppearanceTrait } from './hasAppearance';
|
|
4
|
-
import { createNode } from './node';
|
|
3
|
+
import { initAppearanceRuntimeTrait, initAppearanceTrait } from './hasAppearance';
|
|
4
|
+
import { createNode, createNodeRuntime } from './node';
|
|
5
5
|
|
|
6
6
|
const TestKind = 'Test';
|
|
7
7
|
|
|
@@ -10,6 +10,36 @@ function makeTarget(): HasAppearance {
|
|
|
10
10
|
return node;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
describe('initAppearanceRuntimeTrait', () => {
|
|
14
|
+
let runtime: NodeRuntime<HasAppearance> & HasAppearanceRuntime;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
runtime = createNodeRuntime() as NodeRuntime<HasAppearance> & HasAppearanceRuntime;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('initializes default values', () => {
|
|
21
|
+
initAppearanceRuntimeTrait(runtime);
|
|
22
|
+
|
|
23
|
+
expect(runtime.worldAlpha).toBeNull();
|
|
24
|
+
expect(runtime.worldAlphaUsingAppearanceId).toStrictEqual(-1);
|
|
25
|
+
expect(runtime.worldAlphaUsingParentAppearanceId).toStrictEqual(-1);
|
|
26
|
+
expect(runtime.worldAppearanceId).toStrictEqual(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('resets a resolved cache back to the unresolved state', () => {
|
|
30
|
+
runtime.worldAlpha = 0.25;
|
|
31
|
+
runtime.worldAlphaUsingAppearanceId = 7;
|
|
32
|
+
runtime.worldAlphaUsingParentAppearanceId = 9;
|
|
33
|
+
runtime.worldAppearanceId = 11;
|
|
34
|
+
initAppearanceRuntimeTrait(runtime);
|
|
35
|
+
|
|
36
|
+
expect(runtime.worldAlpha).toBeNull();
|
|
37
|
+
expect(runtime.worldAlphaUsingAppearanceId).toStrictEqual(-1);
|
|
38
|
+
expect(runtime.worldAlphaUsingParentAppearanceId).toStrictEqual(-1);
|
|
39
|
+
expect(runtime.worldAppearanceId).toStrictEqual(0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
13
43
|
describe('initAppearanceTrait', () => {
|
|
14
44
|
it('sets default values when called with no options', () => {
|
|
15
45
|
const target = makeTarget();
|
|
@@ -34,6 +34,8 @@ describe('initTransform2DTrait', () => {
|
|
|
34
34
|
it('initializes default values', () => {
|
|
35
35
|
initTransform2DTrait(node);
|
|
36
36
|
|
|
37
|
+
expect(node.pivotX).toStrictEqual(0);
|
|
38
|
+
expect(node.pivotY).toStrictEqual(0);
|
|
37
39
|
expect(node.rotation).toStrictEqual(0);
|
|
38
40
|
expect(node.scaleX).toStrictEqual(1);
|
|
39
41
|
expect(node.scaleY).toStrictEqual(1);
|
|
@@ -45,6 +47,8 @@ describe('initTransform2DTrait', () => {
|
|
|
45
47
|
|
|
46
48
|
it('allows pre-defined values', () => {
|
|
47
49
|
const base = {
|
|
50
|
+
pivotX: 4,
|
|
51
|
+
pivotY: 8,
|
|
48
52
|
scaleX: 2,
|
|
49
53
|
scaleY: 3,
|
|
50
54
|
skewX: 15,
|
|
@@ -54,6 +58,8 @@ describe('initTransform2DTrait', () => {
|
|
|
54
58
|
y: 200,
|
|
55
59
|
};
|
|
56
60
|
initTransform2DTrait(node, base);
|
|
61
|
+
expect(node.pivotX).toStrictEqual(base.pivotX);
|
|
62
|
+
expect(node.pivotY).toStrictEqual(base.pivotY);
|
|
57
63
|
expect(node.scaleX).toStrictEqual(base.scaleX);
|
|
58
64
|
expect(node.scaleY).toStrictEqual(base.scaleY);
|
|
59
65
|
expect(node.skewX).toStrictEqual(base.skewX);
|
|
@@ -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';
|