@yuneta/gobj-ui 7.23.152 → 7.23.154
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/README.md +24 -3
- package/dist/gobj-ui.cjs.js +345 -27
- package/dist/gobj-ui.es.js +345 -27
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +68 -5
- package/src/treedb_elbow.js +231 -13
- package/src/treedb_elbow.test.js +192 -1
- package/src/treedb_layout.js +146 -14
- package/src/treedb_layout.test.js +84 -12
package/src/treedb_elbow.js
CHANGED
|
@@ -25,6 +25,21 @@
|
|
|
25
25
|
* the order the edges were made, so a new edge never moves the
|
|
26
26
|
* ones already drawn.
|
|
27
27
|
*
|
|
28
|
+
* - Several forward edges sharing one END: the children of one
|
|
29
|
+
* card, or the parents of one card (all its fkeys enter by one
|
|
30
|
+
* point). They are STAGGERED: each turns at its own height in
|
|
31
|
+
* the channel, so their runs lie side by side instead of on top
|
|
32
|
+
* of each other. Leaving a card, the one that runs farthest
|
|
33
|
+
* turns highest; reaching a card, the one that comes from
|
|
34
|
+
* farthest turns lowest -- the two orders in which the lines of
|
|
35
|
+
* one end nest without crossing.
|
|
36
|
+
*
|
|
37
|
+
* - A card in a lower row of a STACK (treedb_layout.js lays a
|
|
38
|
+
* run of leaf children out as two columns with a corridor
|
|
39
|
+
* between them): the card above it stands in the way of the
|
|
40
|
+
* channel, so its edge runs down the corridor in a lane of
|
|
41
|
+
* its own -- a COMB -- and turns into the gap above its card.
|
|
42
|
+
*
|
|
28
43
|
* Written for the top-down reading; left to right is the same
|
|
29
44
|
* geometry with the axes swapped.
|
|
30
45
|
*
|
|
@@ -32,9 +47,12 @@
|
|
|
32
47
|
* All Rights Reserved.
|
|
33
48
|
***********************************************************************/
|
|
34
49
|
|
|
50
|
+
import {STACK_GAP, STACK_LANE_PAD, STACK_LANE_STEP, STACK_TRUNK_MIN} from "./treedb_layout.js";
|
|
51
|
+
|
|
35
52
|
export const ELBOW_STEP = 10; /* between two lanes */
|
|
36
53
|
export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
|
|
37
54
|
export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
|
|
55
|
+
export const ELBOW_STAGGER_PAD = 6; /* staggered turns keep this far from the rows */
|
|
38
56
|
|
|
39
57
|
/************************************************************
|
|
40
58
|
* The lane of `id` among `ids`, the edges joining the same two
|
|
@@ -54,6 +72,24 @@ export function elbow_lane(ids, id)
|
|
|
54
72
|
return side * Math.ceil(k / 2);
|
|
55
73
|
}
|
|
56
74
|
|
|
75
|
+
/************************************************************
|
|
76
|
+
* The rank of `id` among `items` ([{id, span}]): the forward
|
|
77
|
+
* edges that share one end with it, each with how far it runs
|
|
78
|
+
* ACROSS. Farthest first; a tie goes to the edge made first.
|
|
79
|
+
* Returns {rank, count}.
|
|
80
|
+
************************************************************/
|
|
81
|
+
export function elbow_rank(items, id)
|
|
82
|
+
{
|
|
83
|
+
let sorted = items.slice().sort((a, b) => {
|
|
84
|
+
if(b.span !== a.span) {
|
|
85
|
+
return b.span - a.span;
|
|
86
|
+
}
|
|
87
|
+
return String(a.id).localeCompare(String(b.id), undefined, {numeric: true});
|
|
88
|
+
});
|
|
89
|
+
let rank = sorted.findIndex((it) => it.id === id);
|
|
90
|
+
return {rank: (rank < 0)? 0 : rank, count: sorted.length};
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
function swap(p)
|
|
58
94
|
{
|
|
59
95
|
return [p[1], p[0]];
|
|
@@ -64,6 +100,154 @@ function swap_box(b)
|
|
|
64
100
|
return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
|
|
65
101
|
}
|
|
66
102
|
|
|
103
|
+
/* Where a forward edge turns between `lo` and `hi`: the middle, or
|
|
104
|
+
* its staggered place among the edges sharing one of its ends --
|
|
105
|
+
* leaving, the farthest highest; arriving, the farthest lowest. */
|
|
106
|
+
function staggered_turn(lo, hi, st)
|
|
107
|
+
{
|
|
108
|
+
let mid = (lo + hi) / 2;
|
|
109
|
+
let group = null;
|
|
110
|
+
let k = 0;
|
|
111
|
+
if(st.dep && st.dep.count > 1) {
|
|
112
|
+
group = st.dep;
|
|
113
|
+
k = st.dep.rank;
|
|
114
|
+
} else if(st.arr && st.arr.count > 1) {
|
|
115
|
+
group = st.arr;
|
|
116
|
+
k = st.arr.count - 1 - st.arr.rank;
|
|
117
|
+
}
|
|
118
|
+
if(group) {
|
|
119
|
+
let room = Math.max(0, hi - lo - 2 * ELBOW_STAGGER_PAD);
|
|
120
|
+
let step = Math.min(ELBOW_STEP, room / (group.count - 1));
|
|
121
|
+
mid = mid - (group.count - 1) * step / 2 + k * step;
|
|
122
|
+
}
|
|
123
|
+
return mid;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/************************************************************
|
|
127
|
+
* The combs of the stacks under one card.
|
|
128
|
+
*
|
|
129
|
+
* treedb_layout.js lays a run of leaf children out as two columns
|
|
130
|
+
* with a corridor between them. A card in the first row is reached
|
|
131
|
+
* from the channel like any other; one in a lower row cannot be --
|
|
132
|
+
* the card above it stands in the way -- so its edge runs down
|
|
133
|
+
* the corridor in a LANE of its own and turns into the gap above
|
|
134
|
+
* its card, which it shares only with the other card of its row,
|
|
135
|
+
* reached from the other side.
|
|
136
|
+
*
|
|
137
|
+
* targets [{id, box}]: the forward edges leaving one card,
|
|
138
|
+
* each with the box of the card it reaches
|
|
139
|
+
* vertical as in elbow_points; false swaps the boxes
|
|
140
|
+
*
|
|
141
|
+
* Returns Map id -> {lane, z, top}: the lane, the height of the
|
|
142
|
+
* gap it turns into, and the top of the stack's first row (where
|
|
143
|
+
* the channel over the stack ends). Only for a card that stands
|
|
144
|
+
* under another of the group, within a stack gap and lined up
|
|
145
|
+
* with it on the corridor side -- what a stack looks like.
|
|
146
|
+
*
|
|
147
|
+
* Lanes go outermost first, row by row: a line into a higher row
|
|
148
|
+
* keeps outside the lines going deeper, and none crosses another.
|
|
149
|
+
************************************************************/
|
|
150
|
+
export function elbow_combs(targets, vertical)
|
|
151
|
+
{
|
|
152
|
+
let list = vertical? targets : targets.map((it) => ({id: it.id, box: swap_box(it.box)}));
|
|
153
|
+
let cy = (b) => (b.y1 + b.y2) / 2;
|
|
154
|
+
let near = (a, b) => Math.abs(a - b) < 1.5;
|
|
155
|
+
|
|
156
|
+
let under = [];
|
|
157
|
+
for(let it of list) {
|
|
158
|
+
let b = it.box;
|
|
159
|
+
let above = null;
|
|
160
|
+
for(let o of list) {
|
|
161
|
+
let a = o.box;
|
|
162
|
+
if(o === it || a.y2 > b.y1 || b.y1 - a.y2 > STACK_GAP) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if(!(near(a.x2, b.x2) || near(a.x1, b.x1))) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if(!above || a.y2 > above.y2) {
|
|
169
|
+
above = a;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if(!above) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
/* Its column: lined up on the corridor side, which is the
|
|
176
|
+
* right edge of the first column and the left edge of the
|
|
177
|
+
* second. Two cards of one width line up on both; then the
|
|
178
|
+
* card beside it in its row says. */
|
|
179
|
+
let side;
|
|
180
|
+
if(near(above.x2, b.x2) && !near(above.x1, b.x1)) {
|
|
181
|
+
side = 0;
|
|
182
|
+
} else if(near(above.x1, b.x1) && !near(above.x2, b.x2)) {
|
|
183
|
+
side = 1;
|
|
184
|
+
} else {
|
|
185
|
+
let left_of_it = list.some((o) => o !== it && near(cy(o.box), cy(b)) &&
|
|
186
|
+
o.box.x2 <= b.x1);
|
|
187
|
+
side = left_of_it? 1 : 0;
|
|
188
|
+
}
|
|
189
|
+
under.push({id: it.id, box: b, above: above, side: side});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* The corridor of a card: from the first column's edge to the
|
|
193
|
+
* second's, read off any row of the stack that has both. */
|
|
194
|
+
let corridor = (u) => {
|
|
195
|
+
if(u.side === 0) {
|
|
196
|
+
let l = u.box.x2;
|
|
197
|
+
let col = list.filter((o) => near(o.box.x2, l));
|
|
198
|
+
let r = Infinity;
|
|
199
|
+
for(let c of col) {
|
|
200
|
+
for(let o of list) {
|
|
201
|
+
if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x1 >= l - 0.5) {
|
|
202
|
+
r = Math.min(r, o.box.x1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {l: l, r: isFinite(r)? r : l + STACK_TRUNK_MIN, col: col};
|
|
207
|
+
}
|
|
208
|
+
let r = u.box.x1;
|
|
209
|
+
let col = list.filter((o) => near(o.box.x1, r));
|
|
210
|
+
let l = -Infinity;
|
|
211
|
+
for(let c of col) {
|
|
212
|
+
for(let o of list) {
|
|
213
|
+
if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x2 <= r + 0.5) {
|
|
214
|
+
l = Math.max(l, o.box.x2);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {l: isFinite(l)? l : r - STACK_TRUNK_MIN, r: r, col: col};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
let groups = new Map();
|
|
222
|
+
for(let u of under) {
|
|
223
|
+
let c = corridor(u);
|
|
224
|
+
u.top = Math.min(...c.col.map((o) => o.box.y1));
|
|
225
|
+
let key = `${Math.round(c.l)}|${Math.round(c.r)}`;
|
|
226
|
+
if(!groups.has(key)) {
|
|
227
|
+
groups.set(key, {l: c.l, r: c.r, left: [], right: []});
|
|
228
|
+
}
|
|
229
|
+
groups.get(key)[(u.side === 0)? 'left' : 'right'].push(u);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let out = new Map();
|
|
233
|
+
for(let g of groups.values()) {
|
|
234
|
+
g.left.sort((a, b) => a.box.y1 - b.box.y1);
|
|
235
|
+
g.right.sort((a, b) => a.box.y1 - b.box.y1);
|
|
236
|
+
let n = g.left.length + g.right.length;
|
|
237
|
+
let room = g.r - g.l - 2 * STACK_LANE_PAD;
|
|
238
|
+
let step = (n > 1)? Math.max(0, Math.min(STACK_LANE_STEP, room / (n - 1))) : 0;
|
|
239
|
+
g.left.forEach((u, i) => {
|
|
240
|
+
out.set(u.id, {lane: g.l + STACK_LANE_PAD + i * step,
|
|
241
|
+
z: (u.above.y2 + u.box.y1) / 2, top: u.top});
|
|
242
|
+
});
|
|
243
|
+
g.right.forEach((u, j) => {
|
|
244
|
+
out.set(u.id, {lane: g.r - STACK_LANE_PAD - j * step,
|
|
245
|
+
z: (u.above.y2 + u.box.y1) / 2, top: u.top});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
67
251
|
/************************************************************
|
|
68
252
|
* The control points of an elbow from the port `s` to the port
|
|
69
253
|
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
@@ -71,21 +255,39 @@ function swap_box(b)
|
|
|
71
255
|
*
|
|
72
256
|
* vertical the rows are stacked (top to bottom); false
|
|
73
257
|
* when they are side by side (left to right)
|
|
258
|
+
* stagger {dep, arr, comb}: the edge's rank among the
|
|
259
|
+
* forward edges leaving its source card and among
|
|
260
|
+
* those reaching its target card (elbow_rank), and
|
|
261
|
+
* its comb when the target is in a lower row of a
|
|
262
|
+
* stack (elbow_combs). Absent, the channel is the
|
|
263
|
+
* middle.
|
|
74
264
|
************************************************************/
|
|
75
|
-
export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
265
|
+
export function elbow_points(s, t, box_s, box_t, lane, vertical, stagger)
|
|
76
266
|
{
|
|
77
267
|
if(!vertical) {
|
|
78
|
-
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true
|
|
79
|
-
|
|
268
|
+
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
|
|
269
|
+
stagger).map(swap);
|
|
80
270
|
}
|
|
81
271
|
|
|
82
272
|
let shift = lane * ELBOW_STEP;
|
|
83
273
|
|
|
84
|
-
/* Forward: the channel half way
|
|
85
|
-
*
|
|
274
|
+
/* Forward: the channel half way between the two cards, each
|
|
275
|
+
* edge of a shared end at its own height round it, moved by the
|
|
276
|
+
* lane, and kept between the two rows. Into a lower row of a
|
|
277
|
+
* stack: along the channel to its corridor lane, down the lane,
|
|
278
|
+
* and into the gap above its card. */
|
|
86
279
|
if(t[1] - s[1] > 2 * ELBOW_STEP) {
|
|
87
|
-
let
|
|
88
|
-
|
|
280
|
+
let st = stagger || {};
|
|
281
|
+
let lo = Math.max(s[1], box_s.y2);
|
|
282
|
+
if(st.comb) {
|
|
283
|
+
let c = st.comb;
|
|
284
|
+
let turn = staggered_turn(lo, c.top, {dep: st.dep}) + shift;
|
|
285
|
+
turn = Math.min(Math.max(turn, lo + 4), c.top - 4);
|
|
286
|
+
return [[s[0], turn], [c.lane, turn], [c.lane, c.z], [t[0], c.z]];
|
|
287
|
+
}
|
|
288
|
+
let hi = Math.min(t[1], box_t.y1);
|
|
289
|
+
let mid = staggered_turn(lo, hi, st) + shift;
|
|
290
|
+
mid = Math.min(Math.max(mid, lo + 4), hi - 4);
|
|
89
291
|
return [[s[0], mid], [t[0], mid]];
|
|
90
292
|
}
|
|
91
293
|
|
|
@@ -215,8 +417,12 @@ function heap_pop(h)
|
|
|
215
417
|
/************************************************************
|
|
216
418
|
* The shortest orthogonal line from `a` to `z` that enters none
|
|
217
419
|
* of `boxes` (already expanded by the clearance), a turn costing
|
|
218
|
-
* ROUTE_BEND. The line leaves `a`
|
|
219
|
-
*
|
|
420
|
+
* ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
|
|
421
|
+
* SIDEWAYS: the column straight under a port and the one straight
|
|
422
|
+
* over it belong to the forward edges that share that port, which
|
|
423
|
+
* run along them to their staggered turns -- a route going down
|
|
424
|
+
* that column lay on top of them. So a routed line shares only
|
|
425
|
+
* the stub between the port and `a` (or `z`).
|
|
220
426
|
*
|
|
221
427
|
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
222
428
|
* of the two ends, which is all an orthogonal shortest path ever
|
|
@@ -302,7 +508,10 @@ function orth_search(a, z, boxes)
|
|
|
302
508
|
let k = state >> 2;
|
|
303
509
|
let dir = state & 3;
|
|
304
510
|
if(k === goal) {
|
|
305
|
-
|
|
511
|
+
if(dir >= 2) {
|
|
512
|
+
continue; /* down the port's own column: not allowed */
|
|
513
|
+
}
|
|
514
|
+
let total = cost + ROUTE_BEND; /* the turn into the port */
|
|
306
515
|
if(total < found_cost) {
|
|
307
516
|
found_cost = total;
|
|
308
517
|
found = state;
|
|
@@ -310,6 +519,9 @@ function orth_search(a, z, boxes)
|
|
|
310
519
|
continue;
|
|
311
520
|
}
|
|
312
521
|
for(let [nk, nd, len] of moves(k)) {
|
|
522
|
+
if(k === start && nd >= 2) {
|
|
523
|
+
continue; /* out of the port sideways, not down its column */
|
|
524
|
+
}
|
|
313
525
|
let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
|
|
314
526
|
let ns = nk * 4 + nd;
|
|
315
527
|
if(c < best[ns]) {
|
|
@@ -360,15 +572,21 @@ function orth_search(a, z, boxes)
|
|
|
360
572
|
* coincide. Nearby cards first; all of them if that finds no way;
|
|
361
573
|
* the simple elbow if nothing does.
|
|
362
574
|
************************************************************/
|
|
363
|
-
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes)
|
|
575
|
+
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
|
|
364
576
|
{
|
|
365
577
|
let others = boxes || [];
|
|
366
578
|
if(!vertical) {
|
|
367
579
|
return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
|
|
368
|
-
others.map(swap_box)).map(swap);
|
|
580
|
+
others.map(swap_box), stagger).map(swap);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/* A comb runs where the layout left room for it: its corridor
|
|
584
|
+
* and its gap are empty by construction. */
|
|
585
|
+
if(stagger && stagger.comb) {
|
|
586
|
+
return elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
369
587
|
}
|
|
370
588
|
|
|
371
|
-
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
589
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
372
590
|
if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
|
|
373
591
|
return simple;
|
|
374
592
|
}
|
package/src/treedb_elbow.test.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
***********************************************************************/
|
|
8
8
|
import { describe, test, expect } from "vitest";
|
|
9
9
|
import {
|
|
10
|
-
elbow_lane, elbow_points, elbow_route, elbow_path_hits,
|
|
10
|
+
elbow_lane, elbow_points, elbow_route, elbow_path_hits, elbow_rank, elbow_combs,
|
|
11
|
+
ELBOW_STEP, ELBOW_CLEAR,
|
|
11
12
|
} from "./treedb_elbow.js";
|
|
13
|
+
import { layout_compact } from "./treedb_layout.js";
|
|
12
14
|
|
|
13
15
|
/* Two cards 100x40: A on top, B one row below it. */
|
|
14
16
|
const A = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
@@ -165,6 +167,195 @@ describe("the route round the other cards", () => {
|
|
|
165
167
|
});
|
|
166
168
|
});
|
|
167
169
|
|
|
170
|
+
describe("edges sharing an end are staggered", () => {
|
|
171
|
+
test("the rank is farthest first, a tie to the edge made first", () => {
|
|
172
|
+
let items = [{id: "e-3", span: 10}, {id: "e-1", span: 300}, {id: "e-2", span: 10}];
|
|
173
|
+
expect(elbow_rank(items, "e-1")).toEqual({rank: 0, count: 3});
|
|
174
|
+
expect(elbow_rank(items, "e-2")).toEqual({rank: 1, count: 3});
|
|
175
|
+
expect(elbow_rank(items, "e-3")).toEqual({rank: 2, count: 3});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/* One parent on top (port at x 50, bottom at 40) and a row of
|
|
179
|
+
* children below (tops at 130). */
|
|
180
|
+
const P = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
181
|
+
const PORT = [50, 40];
|
|
182
|
+
const KIDS = [50, 160, 290, 420, -120].map((x, i) => ({
|
|
183
|
+
id: `e-${i + 1}`, x: x, box: {x1: x - 50, y1: 130, x2: x + 50, y2: 170},
|
|
184
|
+
}));
|
|
185
|
+
const channel_of = (kid) => {
|
|
186
|
+
let items = KIDS.map((k) => ({id: k.id, span: Math.abs(k.x - PORT[0])}));
|
|
187
|
+
let dep = elbow_rank(items, kid.id);
|
|
188
|
+
return elbow_points(PORT, [kid.x, 130], P, kid.box, 0, true, {dep: dep})[0][1];
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
test("the children of one port turn each at its own height, inside the channel", () => {
|
|
192
|
+
let ys = KIDS.map(channel_of);
|
|
193
|
+
expect(new Set(ys).size).toBe(KIDS.length);
|
|
194
|
+
for(let y of ys) {
|
|
195
|
+
expect(y).toBeGreaterThan(40);
|
|
196
|
+
expect(y).toBeLessThan(130);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("the farthest turns highest, and the lines of one port do not cross", () => {
|
|
201
|
+
let y = Object.fromEntries(KIDS.map((k) => [k.id, channel_of(k)]));
|
|
202
|
+
expect(y["e-4"]).toBeLessThan(y["e-2"]); /* 420 is farther than 160 */
|
|
203
|
+
/* A crossing: one edge's drop (at its child's x, from its turn
|
|
204
|
+
* down to the row) cut by another's run (from the port to its
|
|
205
|
+
* child, at its own turn). */
|
|
206
|
+
for(let a of KIDS) {
|
|
207
|
+
for(let b of KIDS) {
|
|
208
|
+
if(a === b) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
let lo = Math.min(PORT[0], b.x);
|
|
212
|
+
let hi = Math.max(PORT[0], b.x);
|
|
213
|
+
let cuts = a.x > lo && a.x < hi && y[b.id] > y[a.id] && y[b.id] < 130;
|
|
214
|
+
expect(cuts).toBe(false);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("the parents of one card: the farthest turns lowest, and they do not cross", () => {
|
|
220
|
+
/* Parents on top at these x (port at the middle of each bottom),
|
|
221
|
+
* one child below at x 200 whose fkeys all enter at its top. */
|
|
222
|
+
const C = {x1: 150, y1: 130, x2: 250, y2: 170};
|
|
223
|
+
const T0 = [200, 130];
|
|
224
|
+
const PARENTS = [0, 120, 330, 560].map((x, i) => ({id: `p-${i + 1}`, x: x}));
|
|
225
|
+
let items = PARENTS.map((p) => ({id: p.id, span: Math.abs(p.x - T0[0])}));
|
|
226
|
+
let y = {};
|
|
227
|
+
for(let p of PARENTS) {
|
|
228
|
+
let box = {x1: p.x - 50, y1: 0, x2: p.x + 50, y2: 40};
|
|
229
|
+
let arr = elbow_rank(items, p.id);
|
|
230
|
+
y[p.id] = elbow_points([p.x, 40], T0, box, C, 0, true, {arr: arr})[0][1];
|
|
231
|
+
}
|
|
232
|
+
expect(new Set(Object.values(y)).size).toBe(PARENTS.length);
|
|
233
|
+
expect(y["p-4"]).toBeGreaterThan(y["p-2"]); /* 560 is farther than 120 */
|
|
234
|
+
/* A crossing: one parent's drop (at its x, from its row down to
|
|
235
|
+
* its turn) cut by another's run (from its x to the child). */
|
|
236
|
+
for(let a of PARENTS) {
|
|
237
|
+
for(let b of PARENTS) {
|
|
238
|
+
if(a === b) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
let lo = Math.min(b.x, T0[0]);
|
|
242
|
+
let hi = Math.max(b.x, T0[0]);
|
|
243
|
+
let cuts = a.x > lo && a.x < hi && y[b.id] > 40 && y[b.id] < y[a.id];
|
|
244
|
+
expect(cuts).toBe(false);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("a routed edge leaves and reaches its ports sideways, sharing only the stub", () => {
|
|
250
|
+
/* A same-row edge from A to C, with D standing between them:
|
|
251
|
+
* it is routed. It must not run down the column over C's
|
|
252
|
+
* port, which is where a forward edge into C drops. */
|
|
253
|
+
const A2 = {x1: 0, y1: 200, x2: 100, y2: 240};
|
|
254
|
+
const D2 = {x1: 150, y1: 180, x2: 250, y2: 260};
|
|
255
|
+
const C2 = {x1: 300, y1: 200, x2: 400, y2: 240};
|
|
256
|
+
let s = [50, 240];
|
|
257
|
+
let t = [350, 200];
|
|
258
|
+
let boxes = [A2, D2, C2];
|
|
259
|
+
let route = elbow_route(s, t, A2, C2, 0, true, boxes);
|
|
260
|
+
expect(route.length).toBeGreaterThan(2);
|
|
261
|
+
expect(elbow_path_hits([s, ...route, t], boxes, A2, C2)).toBe(false);
|
|
262
|
+
let a = route[0];
|
|
263
|
+
let z = route[route.length - 1];
|
|
264
|
+
expect(route[1][1]).toBe(a[1]); /* leaves a sideways */
|
|
265
|
+
expect(route[route.length - 2][1]).toBe(z[1]); /* reaches z sideways */
|
|
266
|
+
expect(t[1] - z[1]).toBeLessThan(20); /* the stub is short */
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("an end shared by nobody keeps the middle", () => {
|
|
270
|
+
let one = {rank: 0, count: 1};
|
|
271
|
+
expect(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true, {dep: one, arr: one}))
|
|
272
|
+
.toEqual(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true));
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("the comb of a stack", () => {
|
|
277
|
+
/* A parent over eight leaves, laid out by the compact tree (a
|
|
278
|
+
* stack: four rows of two). Boxes as the graph measures them,
|
|
279
|
+
* with 14px ports sticking 7px out of the top and bottom. */
|
|
280
|
+
const P = {id: "p", w: 172, h: 96};
|
|
281
|
+
const LEAVES = [0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({id: `l${i}`, w: 116, h: 40}));
|
|
282
|
+
const pos = layout_compact([P, ...LEAVES], LEAVES.map((l) => ({source: "p", target: l.id})),
|
|
283
|
+
{nodesep: 18, ranksep: 90});
|
|
284
|
+
const box = (n) => {
|
|
285
|
+
let c = pos.get(n.id);
|
|
286
|
+
return {x1: c.x - n.w / 2, y1: c.y - n.h / 2 - 7, x2: c.x + n.w / 2, y2: c.y + n.h / 2 + 7};
|
|
287
|
+
};
|
|
288
|
+
const BOX_P = box(P);
|
|
289
|
+
const PORT = [pos.get("p").x, pos.get("p").y + 48];
|
|
290
|
+
const targets = LEAVES.map((l, i) => ({id: `e-${i + 1}`, box: box(l), leaf: l}));
|
|
291
|
+
const combs = elbow_combs(targets.map((t) => ({id: t.id, box: t.box})), true);
|
|
292
|
+
const paths = targets.map((t) => {
|
|
293
|
+
let comb = combs.get(t.id) || null;
|
|
294
|
+
let items = targets.map((u) => {
|
|
295
|
+
let c = combs.get(u.id);
|
|
296
|
+
let far = c? c.lane : pos.get(u.leaf.id).x;
|
|
297
|
+
return {id: u.id, span: Math.abs(far - PORT[0])};
|
|
298
|
+
});
|
|
299
|
+
let end = [pos.get(t.leaf.id).x, t.box.y1 + 7];
|
|
300
|
+
let pts = elbow_points(PORT, end, BOX_P, t.box, 0, true,
|
|
301
|
+
{dep: elbow_rank(items, t.id), comb: comb});
|
|
302
|
+
return {id: t.id, box: t.box, full: [PORT, ...pts, end]};
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("only the cards under another card get a comb", () => {
|
|
306
|
+
expect(combs.size).toBe(6);
|
|
307
|
+
expect(combs.has("e-1")).toBe(false);
|
|
308
|
+
expect(combs.has("e-2")).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("no line crosses a card", () => {
|
|
312
|
+
let all = [BOX_P, ...targets.map((t) => t.box)];
|
|
313
|
+
for(let p of paths) {
|
|
314
|
+
expect(elbow_path_hits(p.full, all, BOX_P, p.box)).toBe(false);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("no two lines lie on each other past the stub of the port", () => {
|
|
319
|
+
/* Two axis-aligned segments overlap when they are on one line
|
|
320
|
+
* and share more than a point of it. */
|
|
321
|
+
let overlap = (a, b, c, d) => {
|
|
322
|
+
let va = a[0] === b[0];
|
|
323
|
+
let vc = c[0] === d[0];
|
|
324
|
+
let ha = a[1] === b[1];
|
|
325
|
+
let hc = c[1] === d[1];
|
|
326
|
+
if(va && vc && Math.abs(a[0] - c[0]) < 0.5) {
|
|
327
|
+
return Math.min(Math.max(a[1], b[1]), Math.max(c[1], d[1])) -
|
|
328
|
+
Math.max(Math.min(a[1], b[1]), Math.min(c[1], d[1])) > 0.5;
|
|
329
|
+
}
|
|
330
|
+
if(ha && hc && Math.abs(a[1] - c[1]) < 0.5) {
|
|
331
|
+
return Math.min(Math.max(a[0], b[0]), Math.max(c[0], d[0])) -
|
|
332
|
+
Math.max(Math.min(a[0], b[0]), Math.min(c[0], d[0])) > 0.5;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
};
|
|
336
|
+
for(let i = 0; i < paths.length; i++) {
|
|
337
|
+
for(let j = i + 1; j < paths.length; j++) {
|
|
338
|
+
let A = paths[i].full;
|
|
339
|
+
let B = paths[j].full;
|
|
340
|
+
for(let m = 1; m + 1 < A.length; m++) {
|
|
341
|
+
for(let n = 1; n + 1 < B.length; n++) {
|
|
342
|
+
expect(overlap(A[m], A[m + 1], B[n], B[n + 1])).toBe(false);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("a comb is drawn as laid out, not routed", () => {
|
|
350
|
+
let t = targets[6];
|
|
351
|
+
let c = combs.get(t.id);
|
|
352
|
+
let end = [pos.get(t.leaf.id).x, t.box.y1 + 7];
|
|
353
|
+
let st = {comb: c};
|
|
354
|
+
expect(elbow_route(PORT, end, BOX_P, t.box, 0, true, [BOX_P, ...targets.map((u) => u.box)], st))
|
|
355
|
+
.toEqual(elbow_points(PORT, end, BOX_P, t.box, 0, true, st));
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
168
359
|
describe("the ports are obstacles too", () => {
|
|
169
360
|
/* Whole-node boxes: a port of 14px sticks 7px out of the bottom
|
|
170
361
|
* and the top of each card. */
|