@flighthq/node 0.3.0-next.906.07cea63 → 0.3.1-next.209.43ef1bc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boundsRectangle.d.ts +11 -0
- package/dist/boundsRectangle.d.ts.map +1 -1
- package/dist/boundsRectangle.js +50 -4
- 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/hasBoundsRectangle.d.ts +1 -1
- package/dist/hasBoundsRectangle.d.ts.map +1 -1
- package/dist/hasBoundsRectangle.js +1 -0
- package/dist/hasBoundsRectangle.js.map +1 -1
- package/dist/hierarchy.d.ts.map +1 -1
- package/dist/hierarchy.js +19 -3
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -0
- package/dist/node.js.map +1 -1
- package/dist/nodeOrderList.d.ts +88 -0
- package/dist/nodeOrderList.d.ts.map +1 -0
- package/dist/nodeOrderList.js +250 -0
- package/dist/nodeOrderList.js.map +1 -0
- package/dist/revision.d.ts +3 -1
- package/dist/revision.d.ts.map +1 -1
- package/dist/revision.js +8 -1
- package/dist/revision.js.map +1 -1
- package/dist/stageFit.d.ts.map +1 -1
- package/dist/stageFit.js +0 -2
- package/dist/stageFit.js.map +1 -1
- package/dist/traversal.d.ts +1 -1
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +1 -1
- package/dist/traversal.js.map +1 -1
- package/package.json +9 -7
- package/src/boundsRectangle.test.ts +58 -0
- package/src/hasBoundsRectangle.test.ts +8 -0
- package/src/hierarchy.test.ts +75 -1
- package/src/nodeOrderList.test.ts +617 -0
- package/src/revision.test.ts +25 -0
- package/src/traversal.test.ts +0 -1
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
|
+
import type { Node, NodeTraits } from '@flighthq/types/contract';
|
|
3
|
+
import { NodeKind } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import { addNodeChild, removeNodeChild } from './hierarchy';
|
|
6
|
+
import { createNode, enableNodeSignals, getNodeRuntime } from './node';
|
|
7
|
+
import {
|
|
8
|
+
addNodeOrderListEntry,
|
|
9
|
+
applyNodeOrderList,
|
|
10
|
+
clearNodeOrderList,
|
|
11
|
+
createNodeOrderList,
|
|
12
|
+
disposeNodeOrderList,
|
|
13
|
+
forEachNodeOrderListEntry,
|
|
14
|
+
getNodeOrderListEntrySortKey,
|
|
15
|
+
hasNodeOrderListEntry,
|
|
16
|
+
removeNodeOrderListEntry,
|
|
17
|
+
setNodeOrderListEntry,
|
|
18
|
+
setNodeOrderListEntryAbove,
|
|
19
|
+
setNodeOrderListEntryBelow,
|
|
20
|
+
setNodeOrderListFromNodeChildren,
|
|
21
|
+
swapNodeOrderListEntries,
|
|
22
|
+
} from './nodeOrderList';
|
|
23
|
+
import { getNodeChildrenRevision } from './revision';
|
|
24
|
+
|
|
25
|
+
let container: Node<NodeTraits>;
|
|
26
|
+
let childA: Node<NodeTraits>;
|
|
27
|
+
let childB: Node<NodeTraits>;
|
|
28
|
+
let childC: Node<NodeTraits>;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
container = createNode(NodeKind);
|
|
32
|
+
childA = createNode(NodeKind);
|
|
33
|
+
childB = createNode(NodeKind);
|
|
34
|
+
childC = createNode(NodeKind);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function getChildren(source: Node) {
|
|
38
|
+
return getNodeRuntime(source).children as Node[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function attachAll() {
|
|
42
|
+
addNodeChild(container, childA);
|
|
43
|
+
addNodeChild(container, childB);
|
|
44
|
+
addNodeChild(container, childC);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('addNodeOrderListEntry', () => {
|
|
48
|
+
it('appends entries into the valid window', () => {
|
|
49
|
+
const list = createNodeOrderList();
|
|
50
|
+
|
|
51
|
+
addNodeOrderListEntry(list, childA, 5);
|
|
52
|
+
addNodeOrderListEntry(list, childB, 2);
|
|
53
|
+
|
|
54
|
+
expect(list.entryCount).toBe(2);
|
|
55
|
+
expect(list.nodes[0]).toBe(childA);
|
|
56
|
+
expect(list.sortKeys[0]).toBe(5);
|
|
57
|
+
expect(list.nodes[1]).toBe(childB);
|
|
58
|
+
expect(list.sortKeys[1]).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reuses capacity after a clear rather than growing the arrays', () => {
|
|
62
|
+
const list = createNodeOrderList();
|
|
63
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
64
|
+
addNodeOrderListEntry(list, childB, 2);
|
|
65
|
+
clearNodeOrderList(list);
|
|
66
|
+
|
|
67
|
+
addNodeOrderListEntry(list, childC, 3);
|
|
68
|
+
|
|
69
|
+
expect(list.entryCount).toBe(1);
|
|
70
|
+
expect(list.nodes.length).toBe(2);
|
|
71
|
+
expect(list.nodes[0]).toBe(childC);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('allows a node to be entered twice', () => {
|
|
75
|
+
const list = createNodeOrderList();
|
|
76
|
+
|
|
77
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
78
|
+
addNodeOrderListEntry(list, childA, 2);
|
|
79
|
+
|
|
80
|
+
expect(list.entryCount).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('applyNodeOrderList', () => {
|
|
85
|
+
it('permutes members into ascending sort-key order', () => {
|
|
86
|
+
attachAll();
|
|
87
|
+
const list = createNodeOrderList();
|
|
88
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
89
|
+
addNodeOrderListEntry(list, childB, 20);
|
|
90
|
+
addNodeOrderListEntry(list, childC, 10);
|
|
91
|
+
|
|
92
|
+
applyNodeOrderList(container, list);
|
|
93
|
+
|
|
94
|
+
expect(getChildren(container)).toEqual([childC, childB, childA]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('never moves a child the list does not name', () => {
|
|
98
|
+
const foreign = createNode(NodeKind);
|
|
99
|
+
addNodeChild(container, childA);
|
|
100
|
+
addNodeChild(container, foreign);
|
|
101
|
+
addNodeChild(container, childB);
|
|
102
|
+
const list = createNodeOrderList();
|
|
103
|
+
addNodeOrderListEntry(list, childA, 20);
|
|
104
|
+
addNodeOrderListEntry(list, childB, 10);
|
|
105
|
+
|
|
106
|
+
applyNodeOrderList(container, list);
|
|
107
|
+
|
|
108
|
+
// The members swap across the slots they held (0 and 2); the foreign child keeps slot 1, so it
|
|
109
|
+
// still sits between them rather than being compacted to one side.
|
|
110
|
+
expect(getChildren(container)).toEqual([childB, foreign, childA]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('ignores entries that are not children of the target', () => {
|
|
114
|
+
const stranger = createNode(NodeKind);
|
|
115
|
+
addNodeChild(container, childA);
|
|
116
|
+
addNodeChild(container, childB);
|
|
117
|
+
const list = createNodeOrderList();
|
|
118
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
119
|
+
addNodeOrderListEntry(list, stranger, 20);
|
|
120
|
+
addNodeOrderListEntry(list, childB, 10);
|
|
121
|
+
|
|
122
|
+
applyNodeOrderList(container, list);
|
|
123
|
+
|
|
124
|
+
expect(getChildren(container)).toEqual([childB, childA]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('ignores entries attached to a different parent', () => {
|
|
128
|
+
const other = createNode(NodeKind);
|
|
129
|
+
addNodeChild(container, childA);
|
|
130
|
+
addNodeChild(container, childB);
|
|
131
|
+
addNodeChild(other, childC);
|
|
132
|
+
const list = createNodeOrderList();
|
|
133
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
134
|
+
addNodeOrderListEntry(list, childC, 20);
|
|
135
|
+
addNodeOrderListEntry(list, childB, 10);
|
|
136
|
+
|
|
137
|
+
applyNodeOrderList(container, list);
|
|
138
|
+
|
|
139
|
+
expect(getChildren(container)).toEqual([childB, childA]);
|
|
140
|
+
expect(getChildren(other)).toEqual([childC]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('is a no-op once every member has been removed', () => {
|
|
144
|
+
const foreign = createNode(NodeKind);
|
|
145
|
+
addNodeChild(container, childA);
|
|
146
|
+
addNodeChild(container, foreign);
|
|
147
|
+
const list = createNodeOrderList();
|
|
148
|
+
addNodeOrderListEntry(list, childA, 20);
|
|
149
|
+
addNodeOrderListEntry(list, childB, 10);
|
|
150
|
+
removeNodeChild(container, childA);
|
|
151
|
+
const revision = getNodeChildrenRevision(container);
|
|
152
|
+
|
|
153
|
+
applyNodeOrderList(container, list);
|
|
154
|
+
|
|
155
|
+
expect(getChildren(container)).toEqual([foreign]);
|
|
156
|
+
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('sorts the survivors when only some members were removed', () => {
|
|
160
|
+
attachAll();
|
|
161
|
+
const list = createNodeOrderList();
|
|
162
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
163
|
+
addNodeOrderListEntry(list, childB, 20);
|
|
164
|
+
addNodeOrderListEntry(list, childC, 10);
|
|
165
|
+
removeNodeChild(container, childB);
|
|
166
|
+
|
|
167
|
+
applyNodeOrderList(container, list);
|
|
168
|
+
|
|
169
|
+
expect(getChildren(container)).toEqual([childC, childA]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('breaks equal sort keys by the order the entries were added', () => {
|
|
173
|
+
addNodeChild(container, childC);
|
|
174
|
+
addNodeChild(container, childB);
|
|
175
|
+
addNodeChild(container, childA);
|
|
176
|
+
const list = createNodeOrderList();
|
|
177
|
+
addNodeOrderListEntry(list, childA, 7);
|
|
178
|
+
addNodeOrderListEntry(list, childB, 7);
|
|
179
|
+
addNodeOrderListEntry(list, childC, 7);
|
|
180
|
+
|
|
181
|
+
applyNodeOrderList(container, list);
|
|
182
|
+
|
|
183
|
+
expect(getChildren(container)).toEqual([childA, childB, childC]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('resolves a node entered twice to its last entry', () => {
|
|
187
|
+
addNodeChild(container, childA);
|
|
188
|
+
addNodeChild(container, childB);
|
|
189
|
+
const list = createNodeOrderList();
|
|
190
|
+
addNodeOrderListEntry(list, childA, 10);
|
|
191
|
+
addNodeOrderListEntry(list, childB, 20);
|
|
192
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
193
|
+
|
|
194
|
+
applyNodeOrderList(container, list);
|
|
195
|
+
|
|
196
|
+
expect(getChildren(container)).toEqual([childB, childA]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('is idempotent', () => {
|
|
200
|
+
addNodeChild(container, childA);
|
|
201
|
+
addNodeChild(container, childB);
|
|
202
|
+
const list = createNodeOrderList();
|
|
203
|
+
addNodeOrderListEntry(list, childA, 20);
|
|
204
|
+
addNodeOrderListEntry(list, childB, 10);
|
|
205
|
+
applyNodeOrderList(container, list);
|
|
206
|
+
const revision = getNodeChildrenRevision(container);
|
|
207
|
+
|
|
208
|
+
applyNodeOrderList(container, list);
|
|
209
|
+
|
|
210
|
+
expect(getChildren(container)).toEqual([childB, childA]);
|
|
211
|
+
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('advances the children revision once for the whole permutation', () => {
|
|
215
|
+
attachAll();
|
|
216
|
+
const list = createNodeOrderList();
|
|
217
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
218
|
+
addNodeOrderListEntry(list, childB, 20);
|
|
219
|
+
addNodeOrderListEntry(list, childC, 10);
|
|
220
|
+
const revision = getNodeChildrenRevision(container);
|
|
221
|
+
|
|
222
|
+
applyNodeOrderList(container, list);
|
|
223
|
+
|
|
224
|
+
expect(getNodeChildrenRevision(container)).toBe((revision + 1) >>> 0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('calls onChildrenOrderChanged on the parent once', () => {
|
|
228
|
+
attachAll();
|
|
229
|
+
const list = createNodeOrderList();
|
|
230
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
231
|
+
addNodeOrderListEntry(list, childB, 20);
|
|
232
|
+
addNodeOrderListEntry(list, childC, 10);
|
|
233
|
+
let calls = 0;
|
|
234
|
+
connectSignal(enableNodeSignals(container).onChildrenOrderChanged, () => {
|
|
235
|
+
calls++;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
applyNodeOrderList(container, list);
|
|
239
|
+
|
|
240
|
+
expect(calls).toBe(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('does not signal when nothing moved', () => {
|
|
244
|
+
addNodeChild(container, childA);
|
|
245
|
+
addNodeChild(container, childB);
|
|
246
|
+
const list = createNodeOrderList();
|
|
247
|
+
addNodeOrderListEntry(list, childA, 10);
|
|
248
|
+
addNodeOrderListEntry(list, childB, 20);
|
|
249
|
+
let calls = 0;
|
|
250
|
+
connectSignal(enableNodeSignals(container).onChildrenOrderChanged, () => {
|
|
251
|
+
calls++;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
applyNodeOrderList(container, list);
|
|
255
|
+
|
|
256
|
+
expect(calls).toBe(0);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('leaves an empty list alone', () => {
|
|
260
|
+
addNodeChild(container, childA);
|
|
261
|
+
addNodeChild(container, childB);
|
|
262
|
+
const revision = getNodeChildrenRevision(container);
|
|
263
|
+
|
|
264
|
+
applyNodeOrderList(container, createNodeOrderList());
|
|
265
|
+
|
|
266
|
+
expect(getChildren(container)).toEqual([childA, childB]);
|
|
267
|
+
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('clearNodeOrderList', () => {
|
|
272
|
+
it('empties the valid window but keeps capacity', () => {
|
|
273
|
+
const list = createNodeOrderList();
|
|
274
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
275
|
+
|
|
276
|
+
clearNodeOrderList(list);
|
|
277
|
+
|
|
278
|
+
expect(list.entryCount).toBe(0);
|
|
279
|
+
expect(list.nodes.length).toBe(1);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('createNodeOrderList', () => {
|
|
284
|
+
it('creates an empty list', () => {
|
|
285
|
+
const list = createNodeOrderList();
|
|
286
|
+
|
|
287
|
+
expect(list.entryCount).toBe(0);
|
|
288
|
+
expect(list.nodes).toEqual([]);
|
|
289
|
+
expect(list.sortKeys).toEqual([]);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('disposeNodeOrderList', () => {
|
|
294
|
+
it('releases the node references clear deliberately retains', () => {
|
|
295
|
+
const list = createNodeOrderList();
|
|
296
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
297
|
+
addNodeOrderListEntry(list, childB, 2);
|
|
298
|
+
|
|
299
|
+
disposeNodeOrderList(list);
|
|
300
|
+
|
|
301
|
+
expect(list.entryCount).toBe(0);
|
|
302
|
+
expect(list.nodes.length).toBe(0);
|
|
303
|
+
expect(list.sortKeys.length).toBe(0);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('leaves the list usable', () => {
|
|
307
|
+
const list = createNodeOrderList();
|
|
308
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
309
|
+
disposeNodeOrderList(list);
|
|
310
|
+
|
|
311
|
+
addNodeOrderListEntry(list, childB, 4);
|
|
312
|
+
|
|
313
|
+
expect(list.entryCount).toBe(1);
|
|
314
|
+
expect(getNodeOrderListEntrySortKey(list, childB)).toBe(4);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('forEachNodeOrderListEntry', () => {
|
|
319
|
+
it('visits every entry in entry order', () => {
|
|
320
|
+
const list = createNodeOrderList();
|
|
321
|
+
addNodeOrderListEntry(list, childA, 30);
|
|
322
|
+
addNodeOrderListEntry(list, childB, 10);
|
|
323
|
+
const seen: [Node, number, number][] = [];
|
|
324
|
+
|
|
325
|
+
forEachNodeOrderListEntry(list, (node, sortKey, index) => {
|
|
326
|
+
seen.push([node, sortKey, index]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(seen).toEqual([
|
|
330
|
+
[childA, 30, 0],
|
|
331
|
+
[childB, 10, 1],
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('stops early when the callback returns false', () => {
|
|
336
|
+
const list = createNodeOrderList();
|
|
337
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
338
|
+
addNodeOrderListEntry(list, childB, 2);
|
|
339
|
+
let visits = 0;
|
|
340
|
+
|
|
341
|
+
forEachNodeOrderListEntry(list, () => {
|
|
342
|
+
visits++;
|
|
343
|
+
return false;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(visits).toBe(1);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('does not visit entries past the valid window', () => {
|
|
350
|
+
const list = createNodeOrderList();
|
|
351
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
352
|
+
addNodeOrderListEntry(list, childB, 2);
|
|
353
|
+
clearNodeOrderList(list);
|
|
354
|
+
addNodeOrderListEntry(list, childC, 3);
|
|
355
|
+
let visits = 0;
|
|
356
|
+
|
|
357
|
+
forEachNodeOrderListEntry(list, () => {
|
|
358
|
+
visits++;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(visits).toBe(1);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('getNodeOrderListEntrySortKey', () => {
|
|
366
|
+
it('returns the sort key a node is entered with', () => {
|
|
367
|
+
const list = createNodeOrderList();
|
|
368
|
+
addNodeOrderListEntry(list, childA, -4);
|
|
369
|
+
|
|
370
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(-4);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('returns null rather than -1 for a node with no entry', () => {
|
|
374
|
+
const list = createNodeOrderList();
|
|
375
|
+
|
|
376
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBeNull();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('reports the winning entry when a node is entered twice', () => {
|
|
380
|
+
const list = createNodeOrderList();
|
|
381
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
382
|
+
addNodeOrderListEntry(list, childA, 9);
|
|
383
|
+
|
|
384
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(9);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('hasNodeOrderListEntry', () => {
|
|
389
|
+
it('reports whether a node is entered', () => {
|
|
390
|
+
const list = createNodeOrderList();
|
|
391
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
392
|
+
|
|
393
|
+
expect(hasNodeOrderListEntry(list, childA)).toBe(true);
|
|
394
|
+
expect(hasNodeOrderListEntry(list, childB)).toBe(false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('does not see entries past the valid window', () => {
|
|
398
|
+
const list = createNodeOrderList();
|
|
399
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
400
|
+
clearNodeOrderList(list);
|
|
401
|
+
|
|
402
|
+
expect(hasNodeOrderListEntry(list, childA)).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('removeNodeOrderListEntry', () => {
|
|
407
|
+
it('removes the entry and reports that it did', () => {
|
|
408
|
+
const list = createNodeOrderList();
|
|
409
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
410
|
+
addNodeOrderListEntry(list, childB, 2);
|
|
411
|
+
|
|
412
|
+
expect(removeNodeOrderListEntry(list, childA)).toBe(true);
|
|
413
|
+
expect(list.entryCount).toBe(1);
|
|
414
|
+
expect(hasNodeOrderListEntry(list, childA)).toBe(false);
|
|
415
|
+
expect(getNodeOrderListEntrySortKey(list, childB)).toBe(2);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('reports false for a node with no entry', () => {
|
|
419
|
+
const list = createNodeOrderList();
|
|
420
|
+
|
|
421
|
+
expect(removeNodeOrderListEntry(list, childA)).toBe(false);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('shifts the remaining entries down so tie order survives', () => {
|
|
425
|
+
attachAll();
|
|
426
|
+
const list = createNodeOrderList();
|
|
427
|
+
addNodeOrderListEntry(list, childA, 7);
|
|
428
|
+
addNodeOrderListEntry(list, childC, 7);
|
|
429
|
+
addNodeOrderListEntry(list, childB, 7);
|
|
430
|
+
removeNodeOrderListEntry(list, childA);
|
|
431
|
+
|
|
432
|
+
applyNodeOrderList(container, list);
|
|
433
|
+
|
|
434
|
+
// A keeps slot 0 — only its entry was removed, not the child. C was entered before B and stays
|
|
435
|
+
// before it; a swap-with-last removal would have put B first and left the children untouched.
|
|
436
|
+
expect(getChildren(container)).toEqual([childA, childC, childB]);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe('setNodeOrderListEntry', () => {
|
|
441
|
+
it('enters a node that has no entry', () => {
|
|
442
|
+
const list = createNodeOrderList();
|
|
443
|
+
|
|
444
|
+
setNodeOrderListEntry(list, childA, 3);
|
|
445
|
+
|
|
446
|
+
expect(list.entryCount).toBe(1);
|
|
447
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(3);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('re-keys in place rather than adding a second entry', () => {
|
|
451
|
+
const list = createNodeOrderList();
|
|
452
|
+
setNodeOrderListEntry(list, childA, 3);
|
|
453
|
+
|
|
454
|
+
setNodeOrderListEntry(list, childA, 8);
|
|
455
|
+
|
|
456
|
+
expect(list.entryCount).toBe(1);
|
|
457
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(8);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe('setNodeOrderListEntryAbove', () => {
|
|
462
|
+
it('places a node immediately above the target', () => {
|
|
463
|
+
attachAll();
|
|
464
|
+
const list = createNodeOrderList();
|
|
465
|
+
setNodeOrderListFromNodeChildren(list, container);
|
|
466
|
+
|
|
467
|
+
setNodeOrderListEntryAbove(list, childA, childC);
|
|
468
|
+
applyNodeOrderList(container, list);
|
|
469
|
+
|
|
470
|
+
expect(getChildren(container)).toEqual([childB, childC, childA]);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('places between neighbours whose keys leave no gap', () => {
|
|
474
|
+
attachAll();
|
|
475
|
+
const list = createNodeOrderList();
|
|
476
|
+
addNodeOrderListEntry(list, childB, 5);
|
|
477
|
+
addNodeOrderListEntry(list, childC, 6);
|
|
478
|
+
|
|
479
|
+
setNodeOrderListEntryAbove(list, childA, childB);
|
|
480
|
+
applyNodeOrderList(container, list);
|
|
481
|
+
|
|
482
|
+
// No midpoint exists between 5 and 6, so bisection could not express this; the equal-key plus
|
|
483
|
+
// entry-position rule can.
|
|
484
|
+
expect(getChildren(container)).toEqual([childB, childA, childC]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('moves a node that is already entered rather than duplicating it', () => {
|
|
488
|
+
const list = createNodeOrderList();
|
|
489
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
490
|
+
addNodeOrderListEntry(list, childC, 9);
|
|
491
|
+
|
|
492
|
+
setNodeOrderListEntryAbove(list, childA, childC);
|
|
493
|
+
|
|
494
|
+
expect(list.entryCount).toBe(2);
|
|
495
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(9);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('does nothing when the target has no entry', () => {
|
|
499
|
+
const list = createNodeOrderList();
|
|
500
|
+
|
|
501
|
+
setNodeOrderListEntryAbove(list, childA, childC);
|
|
502
|
+
|
|
503
|
+
expect(list.entryCount).toBe(0);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('does nothing when the node is its own target', () => {
|
|
507
|
+
const list = createNodeOrderList();
|
|
508
|
+
addNodeOrderListEntry(list, childA, 4);
|
|
509
|
+
|
|
510
|
+
setNodeOrderListEntryAbove(list, childA, childA);
|
|
511
|
+
|
|
512
|
+
expect(list.entryCount).toBe(1);
|
|
513
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(4);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('setNodeOrderListEntryBelow', () => {
|
|
518
|
+
it('places a node immediately below the target', () => {
|
|
519
|
+
attachAll();
|
|
520
|
+
const list = createNodeOrderList();
|
|
521
|
+
setNodeOrderListFromNodeChildren(list, container);
|
|
522
|
+
|
|
523
|
+
setNodeOrderListEntryBelow(list, childA, childC);
|
|
524
|
+
applyNodeOrderList(container, list);
|
|
525
|
+
|
|
526
|
+
expect(getChildren(container)).toEqual([childB, childA, childC]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('places between neighbours whose keys leave no gap', () => {
|
|
530
|
+
attachAll();
|
|
531
|
+
const list = createNodeOrderList();
|
|
532
|
+
addNodeOrderListEntry(list, childB, 5);
|
|
533
|
+
addNodeOrderListEntry(list, childC, 6);
|
|
534
|
+
|
|
535
|
+
setNodeOrderListEntryBelow(list, childA, childC);
|
|
536
|
+
applyNodeOrderList(container, list);
|
|
537
|
+
|
|
538
|
+
expect(getChildren(container)).toEqual([childB, childA, childC]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('does nothing when the target has no entry', () => {
|
|
542
|
+
const list = createNodeOrderList();
|
|
543
|
+
|
|
544
|
+
setNodeOrderListEntryBelow(list, childA, childC);
|
|
545
|
+
|
|
546
|
+
expect(list.entryCount).toBe(0);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe('setNodeOrderListFromNodeChildren', () => {
|
|
551
|
+
it('enters every child at its own index', () => {
|
|
552
|
+
attachAll();
|
|
553
|
+
const list = createNodeOrderList();
|
|
554
|
+
|
|
555
|
+
setNodeOrderListFromNodeChildren(list, container);
|
|
556
|
+
|
|
557
|
+
expect(list.entryCount).toBe(3);
|
|
558
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(0);
|
|
559
|
+
expect(getNodeOrderListEntrySortKey(list, childB)).toBe(1);
|
|
560
|
+
expect(getNodeOrderListEntrySortKey(list, childC)).toBe(2);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('round-trips through apply without moving anything', () => {
|
|
564
|
+
attachAll();
|
|
565
|
+
const list = createNodeOrderList();
|
|
566
|
+
setNodeOrderListFromNodeChildren(list, container);
|
|
567
|
+
const revision = getNodeChildrenRevision(container);
|
|
568
|
+
|
|
569
|
+
applyNodeOrderList(container, list);
|
|
570
|
+
|
|
571
|
+
expect(getChildren(container)).toEqual([childA, childB, childC]);
|
|
572
|
+
expect(getNodeChildrenRevision(container)).toBe(revision);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('discards any previous contents', () => {
|
|
576
|
+
const stranger = createNode(NodeKind);
|
|
577
|
+
addNodeChild(container, childA);
|
|
578
|
+
const list = createNodeOrderList();
|
|
579
|
+
addNodeOrderListEntry(list, stranger, 99);
|
|
580
|
+
|
|
581
|
+
setNodeOrderListFromNodeChildren(list, container);
|
|
582
|
+
|
|
583
|
+
expect(list.entryCount).toBe(1);
|
|
584
|
+
expect(hasNodeOrderListEntry(list, stranger)).toBe(false);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('empties the list for a node with no children', () => {
|
|
588
|
+
const list = createNodeOrderList();
|
|
589
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
590
|
+
|
|
591
|
+
setNodeOrderListFromNodeChildren(list, childB);
|
|
592
|
+
|
|
593
|
+
expect(list.entryCount).toBe(0);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
describe('swapNodeOrderListEntries', () => {
|
|
598
|
+
it('exchanges the sort keys of two entered nodes', () => {
|
|
599
|
+
attachAll();
|
|
600
|
+
const list = createNodeOrderList();
|
|
601
|
+
setNodeOrderListFromNodeChildren(list, container);
|
|
602
|
+
|
|
603
|
+
swapNodeOrderListEntries(list, childA, childC);
|
|
604
|
+
applyNodeOrderList(container, list);
|
|
605
|
+
|
|
606
|
+
expect(getChildren(container)).toEqual([childC, childB, childA]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('does nothing unless both nodes are entered', () => {
|
|
610
|
+
const list = createNodeOrderList();
|
|
611
|
+
addNodeOrderListEntry(list, childA, 1);
|
|
612
|
+
|
|
613
|
+
swapNodeOrderListEntries(list, childA, childB);
|
|
614
|
+
|
|
615
|
+
expect(getNodeOrderListEntrySortKey(list, childA)).toBe(1);
|
|
616
|
+
});
|
|
617
|
+
});
|
package/src/revision.test.ts
CHANGED
|
@@ -4,9 +4,11 @@ import type { Node, NodeRuntime } from '@flighthq/types/contract';
|
|
|
4
4
|
import {
|
|
5
5
|
computeNodeWorldTransformRevision,
|
|
6
6
|
getNodeAppearanceRevision,
|
|
7
|
+
getNodeChildrenRevision,
|
|
7
8
|
getNodeLocalBoundsRevision,
|
|
8
9
|
getNodeLocalContentRevision,
|
|
9
10
|
getNodeLocalTransformRevision,
|
|
11
|
+
getNodeParentReferenceRevision,
|
|
10
12
|
getNodeWorldTransformRevision,
|
|
11
13
|
invalidateContent,
|
|
12
14
|
invalidateNode,
|
|
@@ -59,6 +61,14 @@ describe('getNodeAppearanceRevision', () => {
|
|
|
59
61
|
});
|
|
60
62
|
});
|
|
61
63
|
|
|
64
|
+
describe('getNodeChildrenRevision', () => {
|
|
65
|
+
it('returns childrenId', () => {
|
|
66
|
+
const runtime = getEntityRuntime(node);
|
|
67
|
+
runtime.childrenId = 100;
|
|
68
|
+
expect(getNodeChildrenRevision(node)).toStrictEqual(runtime.childrenId);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
62
72
|
describe('getNodeLocalBoundsRevision', () => {
|
|
63
73
|
it('returns localBoundsId', () => {
|
|
64
74
|
const runtime = getEntityRuntime(node);
|
|
@@ -83,6 +93,14 @@ describe('getNodeLocalTransformRevision', () => {
|
|
|
83
93
|
});
|
|
84
94
|
});
|
|
85
95
|
|
|
96
|
+
describe('getNodeParentReferenceRevision', () => {
|
|
97
|
+
it('returns parentReferenceId', () => {
|
|
98
|
+
const runtime = getEntityRuntime(node);
|
|
99
|
+
runtime.parentReferenceId = 100;
|
|
100
|
+
expect(getNodeParentReferenceRevision(node)).toStrictEqual(runtime.parentReferenceId);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
86
104
|
describe('getNodeWorldTransformRevision', () => {
|
|
87
105
|
it('returns worldTransformId', () => {
|
|
88
106
|
const runtime = getEntityRuntime(node);
|
|
@@ -193,6 +211,13 @@ describe('invalidateNodeLocalTransform', () => {
|
|
|
193
211
|
});
|
|
194
212
|
|
|
195
213
|
describe('invalidateNodeParentReference', () => {
|
|
214
|
+
it('increments parentReferenceId', () => {
|
|
215
|
+
const runtime = getEntityRuntime(node);
|
|
216
|
+
const parentReferenceId = runtime.parentReferenceId;
|
|
217
|
+
invalidateNodeParentReference(node);
|
|
218
|
+
expect(runtime.parentReferenceId).toBe(parentReferenceId + 1);
|
|
219
|
+
});
|
|
220
|
+
|
|
196
221
|
it('invalidates the world transform parent transform cached ID', () => {
|
|
197
222
|
const runtime = getEntityRuntime(node);
|
|
198
223
|
runtime.worldTransformUsingParentTransformId = 1;
|
package/src/traversal.test.ts
CHANGED