@flighthq/node 0.1.0
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 +26 -0
- package/dist/boundsRectangle.d.ts.map +1 -0
- package/dist/boundsRectangle.js +160 -0
- package/dist/boundsRectangle.js.map +1 -0
- package/dist/hasAppearance.d.ts +3 -0
- package/dist/hasAppearance.d.ts.map +1 -0
- package/dist/hasAppearance.js +6 -0
- package/dist/hasAppearance.js.map +1 -0
- package/dist/hasBoundsRectangle.d.ts +5 -0
- package/dist/hasBoundsRectangle.d.ts.map +1 -0
- package/dist/hasBoundsRectangle.js +9 -0
- package/dist/hasBoundsRectangle.js.map +1 -0
- package/dist/hasClip.d.ts +3 -0
- package/dist/hasClip.d.ts.map +1 -0
- package/dist/hasClip.js +4 -0
- package/dist/hasClip.js.map +1 -0
- package/dist/hasMaterial.d.ts +3 -0
- package/dist/hasMaterial.d.ts.map +1 -0
- package/dist/hasMaterial.js +5 -0
- package/dist/hasMaterial.js.map +1 -0
- package/dist/hasTransform2d.d.ts +4 -0
- package/dist/hasTransform2d.d.ts.map +1 -0
- package/dist/hasTransform2d.js +19 -0
- package/dist/hasTransform2d.js.map +1 -0
- package/dist/hasTransform3d.d.ts +5 -0
- package/dist/hasTransform3d.d.ts.map +1 -0
- package/dist/hasTransform3d.js +8 -0
- package/dist/hasTransform3d.js.map +1 -0
- package/dist/hierarchy.d.ts +127 -0
- package/dist/hierarchy.d.ts.map +1 -0
- package/dist/hierarchy.js +396 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +21 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +109 -0
- package/dist/node.js.map +1 -0
- package/dist/revision.d.ts +40 -0
- package/dist/revision.d.ts.map +1 -0
- package/dist/revision.js +85 -0
- package/dist/revision.js.map +1 -0
- package/dist/transform2d.d.ts +16 -0
- package/dist/transform2d.d.ts.map +1 -0
- package/dist/transform2d.js +99 -0
- package/dist/transform2d.js.map +1 -0
- package/dist/transform3d.d.ts +6 -0
- package/dist/transform3d.d.ts.map +1 -0
- package/dist/transform3d.js +45 -0
- package/dist/transform3d.js.map +1 -0
- package/dist/traversal.d.ts +51 -0
- package/dist/traversal.d.ts.map +1 -0
- package/dist/traversal.js +130 -0
- package/dist/traversal.js.map +1 -0
- package/dist/viewport.d.ts +8 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +78 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +40 -0
- package/src/boundsRectangle.test.ts +497 -0
- package/src/hasAppearance.test.ts +47 -0
- package/src/hasBoundsRectangle.test.ts +57 -0
- package/src/hasClip.test.ts +27 -0
- package/src/hasMaterial.test.ts +30 -0
- package/src/hasTransform2d.test.ts +67 -0
- package/src/hasTransform3d.test.ts +37 -0
- package/src/hierarchy.test.ts +973 -0
- package/src/node.test.ts +265 -0
- package/src/revision.test.ts +211 -0
- package/src/transform2d.test.ts +391 -0
- package/src/transform3d.test.ts +202 -0
- package/src/traversal.test.ts +233 -0
- package/src/viewport.test.ts +228 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { Node, NodeTraits } from '@flighthq/types';
|
|
2
|
+
import { NodeKind } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { addNodeChild } from './hierarchy';
|
|
5
|
+
import { createNode } from './node';
|
|
6
|
+
import {
|
|
7
|
+
findNode,
|
|
8
|
+
findNodeByName,
|
|
9
|
+
forEachNodeAncestor,
|
|
10
|
+
forEachNodeDescendant,
|
|
11
|
+
getNodeChildren,
|
|
12
|
+
getNodeDepth,
|
|
13
|
+
getNodeNextSibling,
|
|
14
|
+
getNodePreviousSibling,
|
|
15
|
+
walkNodeDescendants,
|
|
16
|
+
} from './traversal';
|
|
17
|
+
|
|
18
|
+
let root: Node<NodeTraits>;
|
|
19
|
+
let childA: Node<NodeTraits>;
|
|
20
|
+
let childB: Node<NodeTraits>;
|
|
21
|
+
let grandchild: Node<NodeTraits>;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
root = createNode(NodeKind);
|
|
25
|
+
root.name = 'root';
|
|
26
|
+
childA = createNode(NodeKind);
|
|
27
|
+
childA.name = 'childA';
|
|
28
|
+
childB = createNode(NodeKind);
|
|
29
|
+
childB.name = 'childB';
|
|
30
|
+
grandchild = createNode(NodeKind);
|
|
31
|
+
grandchild.name = 'grandchild';
|
|
32
|
+
|
|
33
|
+
addNodeChild(root, childA);
|
|
34
|
+
addNodeChild(root, childB);
|
|
35
|
+
addNodeChild(childA, grandchild);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('findNode', () => {
|
|
39
|
+
it('returns null for a leaf with no children', () => {
|
|
40
|
+
expect(findNode(grandchild, () => true)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns null when predicate never matches', () => {
|
|
44
|
+
expect(findNode(root, () => false)).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('finds a direct child matching the predicate', () => {
|
|
48
|
+
const result = findNode(root, (n) => n.name === 'childA');
|
|
49
|
+
expect(result).toBe(childA);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('finds a deep descendant matching the predicate', () => {
|
|
53
|
+
const result = findNode(root, (n) => n.name === 'grandchild');
|
|
54
|
+
expect(result).toBe(grandchild);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns the first matching descendant in depth-first order', () => {
|
|
58
|
+
childB.name = 'childA'; // duplicate name
|
|
59
|
+
const result = findNode(root, (n) => n.name === 'childA');
|
|
60
|
+
// childA appears before childB in DFS
|
|
61
|
+
expect(result).toBe(childA);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('does not include the source node itself', () => {
|
|
65
|
+
const result = findNode(root, (n) => n === root);
|
|
66
|
+
expect(result).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('findNodeByName', () => {
|
|
71
|
+
it('returns null when no descendant has the given name', () => {
|
|
72
|
+
expect(findNodeByName(root, 'missing')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('finds a direct child by name', () => {
|
|
76
|
+
expect(findNodeByName(root, 'childA')).toBe(childA);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('finds a deep descendant by name', () => {
|
|
80
|
+
expect(findNodeByName(root, 'grandchild')).toBe(grandchild);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not find the source node itself', () => {
|
|
84
|
+
expect(findNodeByName(root, 'root')).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('forEachNodeAncestor', () => {
|
|
89
|
+
it('does not call callback for a root node', () => {
|
|
90
|
+
const visited: Node<NodeTraits>[] = [];
|
|
91
|
+
forEachNodeAncestor(root, (n) => {
|
|
92
|
+
visited.push(n);
|
|
93
|
+
return true;
|
|
94
|
+
});
|
|
95
|
+
expect(visited).toHaveLength(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('visits ancestors from parent toward root', () => {
|
|
99
|
+
const visited: Node<NodeTraits>[] = [];
|
|
100
|
+
forEachNodeAncestor(grandchild, (n) => {
|
|
101
|
+
visited.push(n);
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
expect(visited).toEqual([childA, root]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('stops early when callback returns false', () => {
|
|
108
|
+
const visited: Node<NodeTraits>[] = [];
|
|
109
|
+
forEachNodeAncestor(grandchild, (n) => {
|
|
110
|
+
visited.push(n);
|
|
111
|
+
return false; // stop after first
|
|
112
|
+
});
|
|
113
|
+
expect(visited).toEqual([childA]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('forEachNodeDescendant', () => {
|
|
118
|
+
it('does not call callback for a leaf node', () => {
|
|
119
|
+
const visited: Node<NodeTraits>[] = [];
|
|
120
|
+
forEachNodeDescendant(grandchild, (n) => visited.push(n));
|
|
121
|
+
expect(visited).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('visits all descendants in depth-first pre-order', () => {
|
|
125
|
+
const visited: Node<NodeTraits>[] = [];
|
|
126
|
+
forEachNodeDescendant(root, (n) => visited.push(n));
|
|
127
|
+
expect(visited).toEqual([childA, grandchild, childB]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('does not include the source node', () => {
|
|
131
|
+
const visited: Node<NodeTraits>[] = [];
|
|
132
|
+
forEachNodeDescendant(root, (n) => visited.push(n));
|
|
133
|
+
expect(visited).not.toContain(root);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('getNodeChildren', () => {
|
|
138
|
+
it('returns an empty array for a node with no children', () => {
|
|
139
|
+
expect(getNodeChildren(grandchild)).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns a snapshot of direct children', () => {
|
|
143
|
+
const children = getNodeChildren(root);
|
|
144
|
+
expect(children).toEqual([childA, childB]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns a copy that does not reflect subsequent mutations', () => {
|
|
148
|
+
const snapshot = getNodeChildren(root);
|
|
149
|
+
const extra = createNode(NodeKind);
|
|
150
|
+
addNodeChild(root, extra);
|
|
151
|
+
expect(snapshot).toHaveLength(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('getNodeDepth', () => {
|
|
156
|
+
it('returns 0 for a root node', () => {
|
|
157
|
+
expect(getNodeDepth(root)).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns 1 for a direct child of the root', () => {
|
|
161
|
+
expect(getNodeDepth(childA)).toBe(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns the correct depth for a deeper node', () => {
|
|
165
|
+
expect(getNodeDepth(grandchild)).toBe(2);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('getNodeNextSibling', () => {
|
|
170
|
+
it('returns null for a root node', () => {
|
|
171
|
+
expect(getNodeNextSibling(root)).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns null for the last child', () => {
|
|
175
|
+
expect(getNodeNextSibling(childB)).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns the next sibling', () => {
|
|
179
|
+
expect(getNodeNextSibling(childA)).toBe(childB);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('getNodePreviousSibling', () => {
|
|
184
|
+
it('returns null for a root node', () => {
|
|
185
|
+
expect(getNodePreviousSibling(root)).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns null for the first child', () => {
|
|
189
|
+
expect(getNodePreviousSibling(childA)).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('returns the previous sibling', () => {
|
|
193
|
+
expect(getNodePreviousSibling(childB)).toBe(childA);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('walkNodeDescendants', () => {
|
|
198
|
+
it('returns true when no children are present', () => {
|
|
199
|
+
expect(walkNodeDescendants(grandchild, () => true)).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('visits all descendants in depth-first pre-order when visitor always returns true', () => {
|
|
203
|
+
const visited: Node<NodeTraits>[] = [];
|
|
204
|
+
const result = walkNodeDescendants(root, (n) => {
|
|
205
|
+
visited.push(n);
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
expect(result).toBe(true);
|
|
209
|
+
expect(visited).toEqual([childA, grandchild, childB]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('stops early and returns false when visitor returns false', () => {
|
|
213
|
+
const visited: Node<NodeTraits>[] = [];
|
|
214
|
+
const result = walkNodeDescendants(root, (n) => {
|
|
215
|
+
visited.push(n);
|
|
216
|
+
return false; // stop at first node
|
|
217
|
+
});
|
|
218
|
+
expect(result).toBe(false);
|
|
219
|
+
expect(visited).toHaveLength(1);
|
|
220
|
+
expect(visited[0]).toBe(childA);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not visit subtree when visitor returns false for a parent', () => {
|
|
224
|
+
const visited: Node<NodeTraits>[] = [];
|
|
225
|
+
walkNodeDescendants(root, (n) => {
|
|
226
|
+
visited.push(n);
|
|
227
|
+
// Stop when we reach childA — its subtree (grandchild) should be skipped
|
|
228
|
+
return n !== childA;
|
|
229
|
+
});
|
|
230
|
+
expect(visited).not.toContain(grandchild);
|
|
231
|
+
expect(visited).not.toContain(childB);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import type { HasBoundsRectangleRuntime } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createNode, getNodeRuntime } from './node';
|
|
5
|
+
import {
|
|
6
|
+
computeViewportAlignX,
|
|
7
|
+
computeViewportAlignY,
|
|
8
|
+
computeViewportFillScale,
|
|
9
|
+
computeViewportFitScale,
|
|
10
|
+
computeViewportRenderTransform,
|
|
11
|
+
createViewport,
|
|
12
|
+
} from './viewport';
|
|
13
|
+
|
|
14
|
+
const TestNodeKind = 'TestNode';
|
|
15
|
+
|
|
16
|
+
function makeNodeWithBounds(width: number, height: number) {
|
|
17
|
+
const node = createNode(TestNodeKind);
|
|
18
|
+
const runtime = getNodeRuntime(node) as unknown as HasBoundsRectangleRuntime;
|
|
19
|
+
runtime.computeLocalBoundsRectangle = (out) => {
|
|
20
|
+
out.width = width;
|
|
21
|
+
out.height = height;
|
|
22
|
+
};
|
|
23
|
+
return node;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('computeViewportAlignX', () => {
|
|
27
|
+
it('returns 0 for left-anchored alignments', () => {
|
|
28
|
+
expect(computeViewportAlignX(400, 800, 'left')).toBe(0);
|
|
29
|
+
expect(computeViewportAlignX(400, 800, 'topleft')).toBe(0);
|
|
30
|
+
expect(computeViewportAlignX(400, 800, 'bottomleft')).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns viewWidth - scaledWidth for right-anchored alignments', () => {
|
|
34
|
+
expect(computeViewportAlignX(400, 800, 'right')).toBe(400);
|
|
35
|
+
expect(computeViewportAlignX(400, 800, 'topright')).toBe(400);
|
|
36
|
+
expect(computeViewportAlignX(400, 800, 'bottomright')).toBe(400);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns centered offset for top/bottom alignments', () => {
|
|
40
|
+
expect(computeViewportAlignX(400, 800, 'top')).toBe(200);
|
|
41
|
+
expect(computeViewportAlignX(400, 800, 'bottom')).toBe(200);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('computeViewportAlignY', () => {
|
|
46
|
+
it('returns 0 for top-anchored alignments', () => {
|
|
47
|
+
expect(computeViewportAlignY(300, 600, 'top')).toBe(0);
|
|
48
|
+
expect(computeViewportAlignY(300, 600, 'topleft')).toBe(0);
|
|
49
|
+
expect(computeViewportAlignY(300, 600, 'topright')).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns viewHeight - scaledHeight for bottom-anchored alignments', () => {
|
|
53
|
+
expect(computeViewportAlignY(300, 600, 'bottom')).toBe(300);
|
|
54
|
+
expect(computeViewportAlignY(300, 600, 'bottomleft')).toBe(300);
|
|
55
|
+
expect(computeViewportAlignY(300, 600, 'bottomright')).toBe(300);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns centered offset for left/right alignments', () => {
|
|
59
|
+
expect(computeViewportAlignY(300, 600, 'left')).toBe(150);
|
|
60
|
+
expect(computeViewportAlignY(300, 600, 'right')).toBe(150);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('computeViewportFillScale', () => {
|
|
65
|
+
it('returns max of width and height ratios', () => {
|
|
66
|
+
expect(computeViewportFillScale(400, 300, 800, 600)).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('uses the larger ratio when width ratio wins', () => {
|
|
70
|
+
expect(computeViewportFillScale(400, 300, 800, 400)).toBeCloseTo(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('uses the larger ratio when height ratio wins', () => {
|
|
74
|
+
expect(computeViewportFillScale(400, 300, 400, 600)).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('computeViewportFitScale', () => {
|
|
79
|
+
it('returns min of width and height ratios', () => {
|
|
80
|
+
expect(computeViewportFitScale(400, 300, 800, 600)).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('uses the smaller ratio when width ratio wins', () => {
|
|
84
|
+
expect(computeViewportFitScale(400, 300, 400, 600)).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('uses the smaller ratio when height ratio wins', () => {
|
|
88
|
+
expect(computeViewportFitScale(400, 300, 800, 400)).toBeCloseTo(400 / 300);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('computeViewportRenderTransform', () => {
|
|
93
|
+
it('sets identity when root is null', () => {
|
|
94
|
+
const m = createMatrix();
|
|
95
|
+
computeViewportRenderTransform(m, createViewport(), 800, 600);
|
|
96
|
+
expect(m.a).toBe(1);
|
|
97
|
+
expect(m.b).toBe(0);
|
|
98
|
+
expect(m.c).toBe(0);
|
|
99
|
+
expect(m.d).toBe(1);
|
|
100
|
+
expect(m.tx).toBe(0);
|
|
101
|
+
expect(m.ty).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('sets identity when root has no bounds capability', () => {
|
|
105
|
+
const m = createMatrix();
|
|
106
|
+
const root = createNode(TestNodeKind);
|
|
107
|
+
computeViewportRenderTransform(m, createViewport({ root }), 800, 600);
|
|
108
|
+
expect(m.a).toBe(1);
|
|
109
|
+
expect(m.d).toBe(1);
|
|
110
|
+
expect(m.tx).toBe(0);
|
|
111
|
+
expect(m.ty).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sets identity when root computeLocalBoundsRectangle returns zero size', () => {
|
|
115
|
+
const m = createMatrix();
|
|
116
|
+
const root = makeNodeWithBounds(0, 0);
|
|
117
|
+
computeViewportRenderTransform(m, createViewport({ root }), 800, 600);
|
|
118
|
+
expect(m.a).toBe(1);
|
|
119
|
+
expect(m.d).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('noscale with topleft: identity scale at origin', () => {
|
|
123
|
+
const m = createMatrix();
|
|
124
|
+
computeViewportRenderTransform(
|
|
125
|
+
m,
|
|
126
|
+
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'noscale', align: 'topleft' }),
|
|
127
|
+
800,
|
|
128
|
+
600,
|
|
129
|
+
);
|
|
130
|
+
expect(m.a).toBe(1);
|
|
131
|
+
expect(m.d).toBe(1);
|
|
132
|
+
expect(m.tx).toBe(0);
|
|
133
|
+
expect(m.ty).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('noscale with top alignment: centers horizontally', () => {
|
|
137
|
+
const m = createMatrix();
|
|
138
|
+
computeViewportRenderTransform(
|
|
139
|
+
m,
|
|
140
|
+
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'noscale', align: 'top' }),
|
|
141
|
+
800,
|
|
142
|
+
600,
|
|
143
|
+
);
|
|
144
|
+
expect(m.a).toBe(1);
|
|
145
|
+
expect(m.d).toBe(1);
|
|
146
|
+
expect(m.tx).toBe(200);
|
|
147
|
+
expect(m.ty).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('exactfit: scales to fill viewport exactly', () => {
|
|
151
|
+
const m = createMatrix();
|
|
152
|
+
computeViewportRenderTransform(
|
|
153
|
+
m,
|
|
154
|
+
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'exactfit', align: 'topleft' }),
|
|
155
|
+
800,
|
|
156
|
+
600,
|
|
157
|
+
);
|
|
158
|
+
expect(m.a).toBe(2);
|
|
159
|
+
expect(m.d).toBe(2);
|
|
160
|
+
expect(m.tx).toBe(0);
|
|
161
|
+
expect(m.ty).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('exactfit: uses independent x/y scales', () => {
|
|
165
|
+
const m = createMatrix();
|
|
166
|
+
computeViewportRenderTransform(
|
|
167
|
+
m,
|
|
168
|
+
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'exactfit', align: 'topleft' }),
|
|
169
|
+
800,
|
|
170
|
+
450,
|
|
171
|
+
);
|
|
172
|
+
expect(m.a).toBe(2);
|
|
173
|
+
expect(m.d).toBe(1.5);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('showall: fits content within viewport with uniform scale', () => {
|
|
177
|
+
const m = createMatrix();
|
|
178
|
+
// viewport 800x400, content 400x300: min(2, 400/300) = 400/300
|
|
179
|
+
computeViewportRenderTransform(
|
|
180
|
+
m,
|
|
181
|
+
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'showall', align: 'topleft' }),
|
|
182
|
+
800,
|
|
183
|
+
400,
|
|
184
|
+
);
|
|
185
|
+
expect(m.a).toBeCloseTo(400 / 300);
|
|
186
|
+
expect(m.d).toBeCloseTo(400 / 300);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('noborder: fills viewport with uniform scale', () => {
|
|
190
|
+
const m = createMatrix();
|
|
191
|
+
// viewport 800x400, content 400x300: max(2, 400/300) = 2
|
|
192
|
+
computeViewportRenderTransform(
|
|
193
|
+
m,
|
|
194
|
+
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'noborder', align: 'topleft' }),
|
|
195
|
+
800,
|
|
196
|
+
400,
|
|
197
|
+
);
|
|
198
|
+
expect(m.a).toBe(2);
|
|
199
|
+
expect(m.d).toBe(2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('sets b and c to 0', () => {
|
|
203
|
+
const m = createMatrix();
|
|
204
|
+
computeViewportRenderTransform(
|
|
205
|
+
m,
|
|
206
|
+
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'showall', align: 'topleft' }),
|
|
207
|
+
800,
|
|
208
|
+
600,
|
|
209
|
+
);
|
|
210
|
+
expect(m.b).toBe(0);
|
|
211
|
+
expect(m.c).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('createViewport', () => {
|
|
216
|
+
it('returns default values', () => {
|
|
217
|
+
const scene = createViewport();
|
|
218
|
+
expect(scene.root).toBeNull();
|
|
219
|
+
expect(scene.scaleMode).toBe('noscale');
|
|
220
|
+
expect(scene.align).toBe('topleft');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('accepts overrides', () => {
|
|
224
|
+
const scene = createViewport({ scaleMode: 'showall', align: 'top' });
|
|
225
|
+
expect(scene.scaleMode).toBe('showall');
|
|
226
|
+
expect(scene.align).toBe('top');
|
|
227
|
+
});
|
|
228
|
+
});
|